diff --git a/enzevalos_iphone.xcodeproj/project.pbxproj b/enzevalos_iphone.xcodeproj/project.pbxproj index aa519c40f4d8c8c5a8d5caa7f11e92e1e856e273..4bdfef76ea68cb6d844d3f2ff6fc30997146fd0a 100644 --- a/enzevalos_iphone.xcodeproj/project.pbxproj +++ b/enzevalos_iphone.xcodeproj/project.pbxproj @@ -67,6 +67,8 @@ 472F398C1E2519C8009260FB /* CNContactExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472F398B1E2519C8009260FB /* CNContactExtension.swift */; }; 472F398E1E251B8D009260FB /* MailAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472F398D1E251B8D009260FB /* MailAddress.swift */; }; 472F39901E252470009260FB /* CNMailAddressesExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472F398F1E252470009260FB /* CNMailAddressesExtension.swift */; }; + 4733B1CE25262CDB00AB5600 /* PersistentDataProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4733B1CD25262CDB00AB5600 /* PersistentDataProtocol.swift */; }; + 4733B1E52527196100AB5600 /* PersistentDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4733B1E42527196100AB5600 /* PersistentDataProvider.swift */; }; 47358D92244A5AEA000116D7 /* SelectableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47358D91244A5AEA000116D7 /* SelectableTextView.swift */; }; 474054982244D7A9007CF83B /* MailServerConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474054972244D7A9007CF83B /* MailServerConfigurationTest.swift */; }; 474994022261E4E6000F8DA5 /* SimpleSendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474994012261E4E6000F8DA5 /* SimpleSendIcon.swift */; }; @@ -411,6 +413,8 @@ 472F398B1E2519C8009260FB /* CNContactExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CNContactExtension.swift; sourceTree = "<group>"; }; 472F398D1E251B8D009260FB /* MailAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailAddress.swift; sourceTree = "<group>"; }; 472F398F1E252470009260FB /* CNMailAddressesExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CNMailAddressesExtension.swift; sourceTree = "<group>"; }; + 4733B1CD25262CDB00AB5600 /* PersistentDataProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentDataProtocol.swift; sourceTree = "<group>"; }; + 4733B1E42527196100AB5600 /* PersistentDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentDataProvider.swift; sourceTree = "<group>"; }; 47358D91244A5AEA000116D7 /* SelectableTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTextView.swift; sourceTree = "<group>"; }; 474054972244D7A9007CF83B /* MailServerConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailServerConfigurationTest.swift; sourceTree = "<group>"; }; 474994012261E4E6000F8DA5 /* SimpleSendIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleSendIcon.swift; sourceTree = "<group>"; }; @@ -1387,6 +1391,8 @@ 47FAE3112524BFDB005A1BCB /* PersistentDataError.swift */, 47FAE31B2524C07B005A1BCB /* CoreMail.swift */, 47FAE3482524FB58005A1BCB /* CoreAddress.swift */, + 4733B1CD25262CDB00AB5600 /* PersistentDataProtocol.swift */, + 4733B1E42527196100AB5600 /* PersistentDataProvider.swift */, ); path = persistentData; sourceTree = "<group>"; @@ -2332,6 +2338,7 @@ 0EF148082422572500B3C198 /* general-helpers.c in Sources */, F113C3851F30D06800E7F1D6 /* QRScannerView.swift in Sources */, 97BDE0432429188500B0BF03 /* BadgeProgressView.swift in Sources */, + 4733B1CE25262CDB00AB5600 /* PersistentDataProtocol.swift in Sources */, 47C822682438A85C005BCE73 /* SenderDetails.swift in Sources */, 477670C6228454F700043604 /* ButtonCell.swift in Sources */, 47C8225824379EAE005BCE73 /* FloatingActionButton.swift in Sources */, @@ -2357,6 +2364,7 @@ 478154A921FF3FF400A931EC /* Invitation.swift in Sources */, 47A2A56E2350A4EF0013883D /* MoreInformationViewController.swift in Sources */, F1AF938F1E2D04BA00755128 /* CustomCells.swift in Sources */, + 4733B1E52527196100AB5600 /* PersistentDataProvider.swift in Sources */, 8428A8711F436A1E007649A5 /* GamificationStatusViewController.swift in Sources */, F1866C86201F707200B72453 /* EmailHelper.m in Sources */, 47F79241203492E3005E7DB6 /* KeyRecord+CoreDataProperties.swift in Sources */, diff --git a/enzevalos_iphone/persistentData/CoreAddress.swift b/enzevalos_iphone/persistentData/CoreAddress.swift index 2693b4c6ba46d546e390bcec7aa8fe600c7dc8e3..913beaa72abb0e5213c553cff37bc38d70799356 100644 --- a/enzevalos_iphone/persistentData/CoreAddress.swift +++ b/enzevalos_iphone/persistentData/CoreAddress.swift @@ -6,13 +6,26 @@ // Copyright © 2020 fu-berlin. All rights reserved. // -extension CoreAddress { - func update(addressProperties: AddressProperties){ +extension CoreAddress: PersistentDataProtocol { + typealias T = AddressProperties + static var entityName = "CoreAddress" + + func update(with addressProperties: AddressProperties){ email = addressProperties.email } } -struct AddressProperties { +struct AddressProperties: DataPropertyProtocol { + var entityName = "CoreAddress" + + func update(m: Any) -> Bool { + if let m = m as? CoreAddress { + m.update(with: self) + return true + } + return false + } + let email: String var dictionary: [String: Any] { diff --git a/enzevalos_iphone/persistentData/CoreMail.swift b/enzevalos_iphone/persistentData/CoreMail.swift index d33f5fff2d06cc4b4cdc14c99bc374be5bf977f9..8e78c79ff01e8d194230fe8149c1aac363432626 100644 --- a/enzevalos_iphone/persistentData/CoreMail.swift +++ b/enzevalos_iphone/persistentData/CoreMail.swift @@ -9,12 +9,16 @@ /** Managed object subclass extension for the Quake entity. */ -extension CoreMail { +extension CoreMail: PersistentDataProtocol { + static let entityName = "CoreMail" func update(with mailProperties: MailProperties) { messageID = mailProperties.messageID subject = mailProperties.subject } + + func update(mail: IncomingMail) { + } } /** @@ -23,13 +27,17 @@ extension CoreMail { What about attached keys (public, secret keys) */ -struct MailProperties { +struct MailProperties: DataPropertyProtocol { + + var entityName = "CoreMail" + // Header properties let messageID: String let subject: String let date: Date let flags: Int16 + let from: AddressProperties // Content properties var body: String = "" @@ -46,4 +54,12 @@ struct MailProperties { dic["subject"] = subject return dic } + + func update(m: Any) -> Bool { + if let m = m as? CoreMail { + m.update(with: self) + return true + } + return false + } } diff --git a/enzevalos_iphone/persistentData/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents b/enzevalos_iphone/persistentData/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents index 07e1b2666fa8bede7fffe2b8255d3fdc33a0df34..bc7537718072638a381cfd352045afaac3ec7119 100644 --- a/enzevalos_iphone/persistentData/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents +++ b/enzevalos_iphone/persistentData/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents @@ -1,21 +1,54 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19H2" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> - <entity name="CoreAddress" representedClassName="CoreAddress" syncable="YES" codeGenerationType="class"> + <entity name="AbstractKeyRecord" representedClassName="AbstractKeyRecord" isAbstract="YES" syncable="YES" codeGenerationType="class"> + <attribute name="cryptoProtocol" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> + <attribute name="id" attributeType="String"/> + </entity> + <entity name="Address" representedClassName="Address" syncable="YES" codeGenerationType="class"> <attribute name="email" attributeType="String"/> <relationship name="inFromField" toMany="YES" deletionRule="Nullify" destinationEntity="CoreMail" inverseName="fromAddress" inverseEntity="CoreMail"/> + <uniquenessConstraints> + <uniquenessConstraint> + <constraint value="email"/> + </uniquenessConstraint> + </uniquenessConstraints> </entity> <entity name="CoreMail" representedClassName="CoreMail" syncable="YES" codeGenerationType="class"> + <attribute name="body" attributeType="String"/> + <attribute name="date" attributeType="Date" usesScalarValueType="NO"/> + <attribute name="encryptionState" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> + <attribute name="flag" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="messageID" attributeType="String"/> - <attribute name="subject" optional="YES" attributeType="String"/> - <relationship name="fromAddress" maxCount="1" deletionRule="Nullify" destinationEntity="CoreAddress" inverseName="inFromField" inverseEntity="CoreAddress"/> + <attribute name="signatureState" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> + <attribute name="subject" attributeType="String"/> + <attribute name="xMailer" optional="YES" attributeType="String"/> + <relationship name="bccAddresses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Address"/> + <relationship name="ccAddresses" toMany="YES" deletionRule="Nullify" destinationEntity="Address"/> + <relationship name="fromAddress" maxCount="1" deletionRule="Nullify" destinationEntity="Address" inverseName="inFromField" inverseEntity="Address"/> + <relationship name="toAddresses" maxCount="1" deletionRule="Nullify" destinationEntity="Address"/> <uniquenessConstraints> <uniquenessConstraint> <constraint value="messageID"/> </uniquenessConstraint> </uniquenessConstraints> </entity> + <entity name="PublicKeyRecord" representedClassName="PublicKeyRecord" syncable="YES" codeGenerationType="class"> + <attribute name="discoveryDate" attributeType="Date" usesScalarValueType="NO"/> + <attribute name="lastSeenInAutocryptHeader" optional="YES" attributeType="Date" usesScalarValueType="NO"/> + <attribute name="lastSeenSignedMail" optional="YES" attributeType="Date" usesScalarValueType="NO"/> + <attribute name="origin" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> + <attribute name="preferEncryption" attributeType="Integer 16" usesScalarValueType="YES"/> + </entity> + <entity name="SecretKeyRecord" representedClassName="SecretKeyRecord" parentEntity="AbstractKeyRecord" syncable="YES" codeGenerationType="class"> + <attribute name="exported" attributeType="Boolean" usesScalarValueType="YES"/> + <attribute name="importedDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/> + </entity> + <fetchRequest name="FetchAddressesRequest" entity="Address" fetchLimit="200" fetchBatchSize="200"/> <elements> - <element name="CoreMail" positionX="-54" positionY="-9" width="128" height="88"/> - <element name="CoreAddress" positionX="-36" positionY="27" width="128" height="73"/> + <element name="Address" positionX="-36" positionY="27" width="128" height="28"/> + <element name="CoreMail" positionX="-54" positionY="-9" width="128" height="28"/> + <element name="AbstractKeyRecord" positionX="-27" positionY="54" width="128" height="28"/> + <element name="SecretKeyRecord" positionX="-9" positionY="63" width="128" height="28"/> + <element name="PublicKeyRecord" positionX="9" positionY="63" width="128" height="28"/> </elements> </model> \ No newline at end of file diff --git a/enzevalos_iphone/persistentData/DataProvider.swift b/enzevalos_iphone/persistentData/DataProvider.swift index f701c9ec3edea842a9cef74ab0bb0f1098549f41..f5495661c974775e04b36b8eeb3f2e09e39613b2 100644 --- a/enzevalos_iphone/persistentData/DataProvider.swift +++ b/enzevalos_iphone/persistentData/DataProvider.swift @@ -7,17 +7,13 @@ // import CoreData -let MAILNAME = "CoreMail" - /* One Entity needs a name, type, a struct with dict and a update function with the struct... */ public class DataProvider { - - let fastImport = false - + // MARK: - Core Data /** @@ -64,9 +60,14 @@ public class DataProvider { return taskContext } + func importMails(from emails: [MailProperties], completionHandler: @escaping (Error?) -> Void, withBIR: Bool = true){ + importData(from: emails, completionHandler: completionHandler, withBIR: withBIR) + } - func importMails(from emails: [MailProperties], completionHandler: @escaping (Error?) -> Void, withBIR: Bool = true) { - guard !emails.isEmpty else { + + + func importData(from data: [DataPropertyProtocol], completionHandler: @escaping (Error?) -> Void, withBIR: Bool = true) { + guard !data.isEmpty else { completionHandler(nil) return } @@ -75,10 +76,10 @@ public class DataProvider { DispatchQueue.global(qos: .background).async { do { if withBIR { - try self.importMailsUsingBIR(from: emails) + try self.importDataUsingBIR(from: data) } else { - try self.importEmailsBeforeBIR(from: emails) + try self.importDataBeforeBIR(from: data) } } catch { performError = error @@ -95,17 +96,17 @@ public class DataProvider { Uses NSBatchInsertRequest (BIR) to import a list of emails into the Core Data store on a private queue. NSBatchInsertRequest is available since iOS 13 and macOS 10.15. */ - private func importMailsUsingBIR(from emails: [MailProperties]) throws { + private func importDataUsingBIR(from data: [DataPropertyProtocol]) throws { var performError: Error? let taskContext = self.newTaskContext() taskContext.performAndWait { - let batchInsert = self.newBatchInsertRequest(with: emails) - batchInsert.resultType = .statusOnly - - if let batchInsertResult = try? taskContext.execute(batchInsert) as? NSBatchInsertResult, - let success = batchInsertResult.result as? Bool, success { - return + if let batchInsert = self.newBatchInsertRequest(with: data) { + batchInsert.resultType = .statusOnly + if let batchInsertResult = try? taskContext.execute(batchInsert) as? NSBatchInsertResult, + let success = batchInsertResult.result as? Bool, success { + return + } } performError = PersistentDataError.batchInsertError } @@ -114,13 +115,17 @@ public class DataProvider { } } - private func newBatchInsertRequest(with mails: [MailProperties]) -> NSBatchInsertRequest { + private func newBatchInsertRequest(with data: [DataPropertyProtocol]) -> NSBatchInsertRequest? { + guard data.count > 0 else { + return nil + } + let name = data.first!.entityName let batchInsert: NSBatchInsertRequest var index = 0 - let total = mails.count - batchInsert = NSBatchInsertRequest(entityName: MAILNAME, dictionaryHandler: { dictionary in + let total = data.count + batchInsert = NSBatchInsertRequest(entityName: name, dictionaryHandler: { dictionary in guard index < total else { return true } - dictionary.addEntries(from: mails[index].dictionary) + dictionary.addEntries(from: data[index].dictionary) index += 1 return false }) @@ -131,12 +136,12 @@ public class DataProvider { Imports a JSON dictionary into the Core Data store on a private queue, processing the record in batches to avoid a high memory footprint. */ - private func importEmailsBeforeBIR(from emails: [MailProperties]) throws { - guard !emails.isEmpty else { return } + private func importDataBeforeBIR(from data: [DataPropertyProtocol]) throws { + guard !data.isEmpty else { return } // Process records in batches to avoid a high memory footprint. let batchSize = 256 - let count = emails.count + let count = data.count // Determine the total number of batches. var numBatches = count / batchSize @@ -150,7 +155,7 @@ public class DataProvider { // Create a batch for this range from the decoded JSON. // Stop importing if any batch is unsuccessful. - try importOneBatch(Array(emails[range])) + try importOneBatch(Array(data[range])) } } @@ -163,21 +168,25 @@ public class DataProvider { catches throws within the closure and uses a return value to indicate whether the import is successful. */ - private func importOneBatch(_ emails: [MailProperties]) throws { + private func importOneBatch(_ data: [DataPropertyProtocol]) throws { + guard data.count > 0 else { + throw PersistentDataError.creationError + } + + let name = data.first!.entityName + let taskContext = newTaskContext() var performError: Error? - // taskContext.performAndWait runs on the URLSession's delegate queue - // so it won’t block the main thread. taskContext.performAndWait { // Create a new record for each quake in the batch. - for m in emails { - // Create a Quake managed object on the private queue context. - guard let email = NSEntityDescription.insertNewObject(forEntityName: MAILNAME, into: taskContext) as? CoreMail else { + for m in data { + // Create a managed object on the private queue context. + let item = NSEntityDescription.insertNewObject(forEntityName: name, into: taskContext) + if !m.update(m: item) { performError = PersistentDataError.creationError return } - email.update(with: m) } // Save all insertions and deletions from the context to the store. @@ -204,23 +213,37 @@ public class DataProvider { func deleteAll(completionHandler: @escaping (Error?) -> Void) { let taskContext = newTaskContext() taskContext.perform { - let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: MAILNAME) - let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - batchDeleteRequest.resultType = .resultTypeCount - - // Execute the batch insert - if let batchDeleteResult = try? taskContext.execute(batchDeleteRequest) as? NSBatchDeleteResult, - batchDeleteResult.result != nil { - completionHandler(nil) - - } else { - completionHandler(PersistentDataError.batchDeleteError) + for name in [CoreAddress.entityName, CoreMail.entityName] { + let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: name) + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + batchDeleteRequest.resultType = .resultTypeCount + + // Execute the batch insert + guard let batchDeleteResult = try? taskContext.execute(batchDeleteRequest) as? NSBatchDeleteResult, + batchDeleteResult.result != nil else { + completionHandler(PersistentDataError.batchDeleteError) + return + } } + completionHandler(nil) } } // MARK: - NSFetchedResultsController + lazy var fetchAddressResultController: NSFetchedResultsController<CoreAddress> = { + let freq = NSFetchRequest<CoreAddress>(entityName: CoreAddress.entityName) + freq.sortDescriptors = [NSSortDescriptor(key: "email", ascending: false)] + + let controller = NSFetchedResultsController(fetchRequest: freq, managedObjectContext: persistentContainer.viewContext, sectionNameKeyPath: nil, cacheName: nil) + do { + try controller.performFetch() + } catch { + fatalError("Unresolved error \(error)") + } + return controller + }() + /** A fetched results controller delegate to give consumers a chance to update the user interface when content changes. @@ -228,12 +251,12 @@ public class DataProvider { weak var fetchedResultsControllerDelegate: NSFetchedResultsControllerDelegate? /** - A fetched results controller to fetch Quake records sorted by time. + A fetched results controller to fetch EMail records sorted by time. */ lazy var fetchedResultsController: NSFetchedResultsController<CoreMail> = { // Create a fetch request for the Quake entity sorted by time. - let fetchRequest = NSFetchRequest<CoreMail>(entityName: MAILNAME) + let fetchRequest = NSFetchRequest<CoreMail>(entityName: CoreMail.entityName) // Create a fetched results controller and set its fetch request, context, and delegate. fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageID", ascending: false)] fetchRequest.propertiesToFetch = ["messageID", "subject"] @@ -255,7 +278,7 @@ public class DataProvider { Resets viewContext and refetches the data from the store. */ func resetAndRefetch() { - persistentContainer.viewContext.reset() + reset() do { try fetchedResultsController.performFetch() } catch { @@ -263,6 +286,10 @@ public class DataProvider { } } + func reset(){ + persistentContainer.viewContext.reset() + } + // MARK: - NSPersistentStoreRemoteChange handler /** diff --git a/enzevalos_iphone/persistentData/PersistentDataProtocol.swift b/enzevalos_iphone/persistentData/PersistentDataProtocol.swift new file mode 100644 index 0000000000000000000000000000000000000000..954e57af691c48f622caa29718a1ebd0ad8eb183 --- /dev/null +++ b/enzevalos_iphone/persistentData/PersistentDataProtocol.swift @@ -0,0 +1,23 @@ +// +// PersistentDataProtocol.swift +// enzevalos_iphone +// +// Created by Oliver Wiese on 01.10.20. +// Copyright © 2020 fu-berlin. All rights reserved. +// + +protocol PersistentDataProtocol { + associatedtype T + + func update(with properties: T) +} + +protocol DataPropertyProtocol { + var entityName: String { get } + var dictionary: [String: Any] { get } + + func update(m: Any) -> Bool +} + + + diff --git a/enzevalos_iphone/persistentData/PersistentDataProvider.swift b/enzevalos_iphone/persistentData/PersistentDataProvider.swift new file mode 100644 index 0000000000000000000000000000000000000000..36e63744f81ef570a83cca0eefca5706f5e34669 --- /dev/null +++ b/enzevalos_iphone/persistentData/PersistentDataProvider.swift @@ -0,0 +1,154 @@ +// +// PersistentDataProvider.swift +// enzevalos_iphone +// +// Created by Oliver Wiese on 02.10.20. +// Copyright © 2020 fu-berlin. All rights reserved. +// + +import CoreData + +class PersitentDataProvider { + // MARK: - Core Data + + /** + A persistent container to set up the Core Data stack. + */ + lazy var persistentContainer: NSPersistentContainer = { + let container = NSPersistentContainer(name: "DataModel") + + // Enable remote notifications + guard let description = container.persistentStoreDescriptions.first else { + fatalError("Failed to retrieve a persistent store description.") + } + description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) + + container.loadPersistentStores { storeDesription, error in + guard error == nil else { + fatalError("Unresolved error \(error!)") + } + } + + // This sample refreshes UI by refetching data, so doesn't need to merge the changes. + container.viewContext.automaticallyMergesChangesFromParent = false + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + container.viewContext.undoManager = nil + container.viewContext.shouldDeleteInaccessibleFaults = true + + // Observe Core Data remote change notifications. + NotificationCenter.default.addObserver( + self, selector: #selector(type(of: self).storeRemoteChange(_:)), + name: .NSPersistentStoreRemoteChange, object: nil) + return container + }() + + /** + Creates and configures a private queue context. + */ + private func newTaskContext() -> NSManagedObjectContext { + // Create a private queue context. + let taskContext = persistentContainer.newBackgroundContext() + taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + // Set unused undoManager to nil for macOS (it is nil by default on iOS) + // to reduce resource requirements. + taskContext.undoManager = nil + return taskContext + } + + + func importMails(from mails: [MailProperties], completionHandler: @escaping (Error?) -> Void) { + guard !mails.isEmpty else { + completionHandler(nil) + return + } + var performError: Error? = nil + + DispatchQueue.global(qos: .background).async { + // Process records in batches to avoid a high memory footprint. + let batchSize = 30 + let count = mails.count + + if count < batchSize { + do { + try self.importOneBatch(mails) + } catch { + performError = error + } + } else { + // Determine the total number of batches. + var numBatches = count / batchSize + numBatches += count % batchSize > 0 ? 1 : 0 + + for batchNumber in 0 ..< numBatches { + // Determine the range for this batch. + let batchStart = batchNumber * batchSize + let batchEnd = batchStart + min(batchSize, count - batchNumber * batchSize) + let range = batchStart..<batchEnd + + // Create a batch for this range from the decoded JSON. + // Stop importing if any batch is unsuccessful. + do { + try self.importOneBatch(Array(mails[range])) + } catch { + performError = error + } + } + } + + DispatchQueue.main.asyncAfter(deadline: .now(), execute: { + completionHandler(performError) + }) + + } + } + + private func importOneBatch(_ mails: [MailProperties]) throws { + guard mails.count > 0 else { + throw PersistentDataError.creationError + } + + let taskContext = newTaskContext() + var performError: Error? + + taskContext.performAndWait { + // Create a new record for each mail in the batch. + for m in mails { + // Create a managed object on the private queue context. + let from = NSEntityDescription.insertNewObject(forEntityName: CoreAddress.entityName, into: taskContext) as! CoreAddress + from.update(with: m.from) + + let mail = NSEntityDescription.insertNewObject(forEntityName: CoreMail.entityName, into: taskContext) as! CoreMail + mail.fromAddress = from + mail.messageID = m.messageID + + // add relationship -> Update? + + } + + // Save all insertions and deletions from the context to the store. + if taskContext.hasChanges { + do { + try taskContext.save() + } catch { + performError = error + return + } + // Reset the taskContext to free the cache and lower the memory footprint. + taskContext.reset() + } + } + + if let error = performError { + throw error + } + } + + /** + Handles remote store change notifications (.NSPersistentStoreRemoteChange). + storeRemoteChange runs on the queue where the changes were made. + */ + @objc + func storeRemoteChange(_ notification: Notification) { + //print("\(#function): Got a persistent store remote change notification!") + } +} diff --git a/enzevalos_iphoneTests/CoreAddressTest.swift b/enzevalos_iphoneTests/CoreAddressTest.swift index 4f6aaf27f05a93c75d3d3fe70c6e07c7981f7941..4cffb4a1bde92f93a66c8673b455fa58852d8d60 100644 --- a/enzevalos_iphoneTests/CoreAddressTest.swift +++ b/enzevalos_iphoneTests/CoreAddressTest.swift @@ -21,16 +21,28 @@ class CoreAddressTest: XCTestCase { } func testCreateAddress() throws { + let n = 1 + let addr1 = AddressProperties(email: "vic@example.com") + let addr2 = AddressProperties(email: "vic@example.com") + let importExpectation = expectation(description: "Import new addresses!") + let addresses = [addr1, addr2] - let addresses = [addr1] - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. + provider.importData(from: addresses, completionHandler: {error in + if let error = error { + XCTFail("Error while importing addresses! \(error)") + } + importExpectation.fulfill() + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) + provider.reset() + let frc = provider.fetchAddressResultController + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, n, "Missing message! \(frc.fetchedObjects?.count ?? 0)") + if let objects = frc.fetchedObjects { + for obj in objects { + print("#from mails: \(obj.inFromField?.count ?? -1)") + } } + } - } diff --git a/enzevalos_iphoneTests/CoreMailTest.swift b/enzevalos_iphoneTests/CoreMailTest.swift index 79574a89f647085160a835ea4017c16bf0e97ec7..f721f083c1ecfeac42f0eabfeb58a84d9993ae39 100644 --- a/enzevalos_iphoneTests/CoreMailTest.swift +++ b/enzevalos_iphoneTests/CoreMailTest.swift @@ -12,6 +12,7 @@ import XCTest class CoreMailTest: XCTestCase { var provider = DataProvider() + var p2 = PersitentDataProvider() override func setUpWithError() throws { provider.deleteAll(completionHandler: {_ in }) @@ -20,7 +21,74 @@ class CoreMailTest: XCTestCase { override func tearDownWithError() throws { provider.deleteAll(completionHandler: {_ in }) } + + func testImportMail(){ + let addr = "vic@example.com" + let addr1 = AddressProperties(email: addr) + let importExpectation = expectation(description: "Import new msg!") + + let m1 = MailProperties(messageID: "1", subject: "MSG1", date: Date(), flags: 0, from: addr1, body: "THIS IS MY SECOND MSG1", signatureState: 0, encryptionState: 0) + let m2 = MailProperties(messageID: "2", subject: "MSG2", date: Date(), flags: 0, from: addr1, body: "THIS IS MY SECOND MSG1", signatureState: 0, encryptionState: 0) + p2.importMails(from: [m1,m2], completionHandler: {error in + if let error = error { + XCTFail("Error while importing messages! \(error)") + } + importExpectation.fulfill()}) + wait(for: [importExpectation], timeout: TimeInterval(20)) + self.provider.resetAndRefetch() + if let mails = provider.fetchedResultsController.fetchedObjects { + for m in mails { + XCTAssertEqual(m.fromAddress?.email ?? "", addr, "Wrong email from address: \(m.fromAddress?.email ?? "-1")") + } + } + else { + XCTFail("No mails...") + } + + let frc = provider.fetchAddressResultController + if let objects = frc.fetchedObjects { + for obj in objects { + XCTAssertEqual(obj.inFromField?.count, 2, "Missing mails.. \(String(describing: obj.inFromField?.count))") + } + } else { + XCTFail("No addresses...") + } + + + } + + func testImportOneMail() { + let addr1 = AddressProperties(email: "vic@example.com") + let importExpectation = expectation(description: "Import new addresses!") + let addresses = [addr1] + + provider.importData(from: addresses, completionHandler: {error in + if let error = error { + XCTFail("Error while importing addresses! \(error)") + } + let m = MailProperties(messageID: "1", subject: "MSG1", date: Date(), flags: 0, from: addr1, body: "THIS IS MY SECOND MSG1", signatureState: 0, encryptionState: 0) + self.provider.importMails(from: [m], completionHandler: {error in + if let error = error { + XCTFail("Error while importing messages! \(error)") + } + importExpectation.fulfill() + }, withBIR: true) + + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) + self.provider.resetAndRefetch() + if let mails = provider.fetchedResultsController.fetchedObjects { + for m in mails { + print(m.messageID, m.subject, m.fromAddress?.email) + } + } + else { + XCTFail("No mails...") + } + + } +/* func testImport(withBIR: Bool) { let n = 100 @@ -45,6 +113,7 @@ class CoreMailTest: XCTestCase { provider.resetAndRefetch() XCTAssertEqual(provider.fetchedResultsController.fetchedObjects?.count ?? 0, n, "Missing message! \(provider.fetchedResultsController.fetchedObjects?.count ?? 0)") } + func testImportWithBIR() { testImport(withBIR: true) @@ -53,4 +122,5 @@ class CoreMailTest: XCTestCase { func testImportBeforeBIR() { testImport(withBIR: false) } +*/ } diff --git a/enzevalos_iphoneTests/GeneratedMocks.swift b/enzevalos_iphoneTests/GeneratedMocks.swift index eaaae2ddb9212b1c31dae63385ea42d08b295b6c..20fda625749c80505919b8abe49ee279ea10ba6c 100644 --- a/enzevalos_iphoneTests/GeneratedMocks.swift +++ b/enzevalos_iphoneTests/GeneratedMocks.swift @@ -1,4 +1,4 @@ -// MARK: - Mocks generated from file: enzevalos_iphone/AuthenticationModel.swift at 2020-09-30 17:29:22 +0000 +// MARK: - Mocks generated from file: enzevalos_iphone/AuthenticationModel.swift at 2020-10-02 10:21:42 +0000 // // AuthenticationModel.swift @@ -654,7 +654,7 @@ import Foundation } -// MARK: - Mocks generated from file: enzevalos_iphone/AuthenticationViewModel.swift at 2020-09-30 17:29:22 +0000 +// MARK: - Mocks generated from file: enzevalos_iphone/AuthenticationViewModel.swift at 2020-10-02 10:21:42 +0000 // // AuthenticationViewModel.swift