OSDN Git Service

e6d7e6fde593f9103c82a4bad55ddc4efdb1ea41
[kcd/KCD.git] / KCD / CoreDataCore.swift
1 //
2 //  CoreDataManager.swift
3 //  KCD
4 //
5 //  Created by Hori,Masaki on 2017/02/05.
6 //  Copyright © 2017年 Hori,Masaki. All rights reserved.
7 //
8
9 import Cocoa
10
11 enum CoreDataManagerType {
12     case reader
13     case editor
14 }
15
16 enum CoreDataError: Error {
17     case applicationDirectoryIsFile
18     case couldNotCreateModel
19     case couldNotCreateCoordinator(String)
20 }
21
22 struct CoreDataIntormation {
23     let modelName: String
24     let fileName: String
25     let options: [AnyHashable: Any]
26     let type: String
27     let tryRemake: Bool
28     
29     private static let defaultOptions: [AnyHashable: Any] = [
30         NSMigratePersistentStoresAutomaticallyOption: true,
31         NSInferMappingModelAutomaticallyOption: true
32     ]
33     
34     init(_ modelName: String,
35          fileName: String? = nil,
36          options: [AnyHashable: Any] = defaultOptions,
37          type: String = NSSQLiteStoreType,
38          tryRemake: Bool = false) {
39         self.modelName = modelName
40         self.fileName = fileName ?? "\(modelName).storedata"
41         self.options = options
42         self.type = type
43         self.tryRemake = tryRemake
44     }
45 }
46
47 struct CoreDataCore {
48     let info: CoreDataIntormation
49     let model: NSManagedObjectModel
50     let coordinator: NSPersistentStoreCoordinator
51     let parentContext: NSManagedObjectContext
52     
53     init(_ info: CoreDataIntormation) {
54         self.info = info
55         do {
56             let genaratee = try MocGenerater.genarate(info)
57             (self.model, self.coordinator, self.parentContext) = genaratee
58         } catch {
59             fatalError("CoreDataCore: can not initialize. \(error)")
60         }
61     }
62     
63     func editorContext() -> NSManagedObjectContext {
64         let moc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
65         moc.parent = parentContext
66         moc.undoManager = nil
67         return moc
68     }
69 }
70
71
72 protocol CoreDataProvider {
73     init(type: CoreDataManagerType)
74     var core: CoreDataCore { get }
75     var context: NSManagedObjectContext { get }
76     func save()
77 }
78
79 protocol CoreDataManager {
80     associatedtype InstanceType = Self
81     
82     static var `default`: InstanceType { get }
83     static func oneTimeEditor() -> InstanceType
84     
85     func removeDataFile()
86 }
87
88 protocol CoreDataAccessor: CoreDataProvider {
89     func insertNewObject<T>(for entity: Entity<T>) -> T?
90     func delete(_ object: NSManagedObject)
91     func object(with objectId: NSManagedObjectID) -> NSManagedObject
92     func objects<T>(with entity: Entity<T>, sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?) throws -> [T]
93 }
94
95 private class CoreDataRemover {
96     class func remove(name: String) {
97         ["", "-wal", "-shm"]
98             .map { name + $0 }
99             .map { ApplicationDirecrories.support.appendingPathComponent($0) }
100             .forEach { removeDataFile(at: $0) }
101     }
102     private class func removeDataFile(at url: URL) {
103         do {
104             try FileManager.default.removeItem(at: url)
105         } catch {
106             print("Could not remove file for URL (\(url))")
107         }
108     }
109 }
110
111 private class MocGenerater {
112     class func genarate(_ info: CoreDataIntormation) throws ->
113         (model: NSManagedObjectModel, coordinator: NSPersistentStoreCoordinator, moc: NSManagedObjectContext) {
114             do {
115                 let model = try createManagedObjectModel(info)
116                 let coordinator = try createPersistentStoreCoordinator(info, model)
117                 let moc = createManagedObjectContext(coordinator)
118                 return (model: model, coordinator: coordinator, moc: moc)
119             } catch {
120                 throw error
121             }
122     }
123     
124     private class func createManagedObjectModel(_ info: CoreDataIntormation) throws -> NSManagedObjectModel {
125         let modelURL = Bundle.main.url(forResource: info.modelName, withExtension: "momd")!
126         guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
127             throw CoreDataError.couldNotCreateModel
128         }
129         return model
130     }
131     // swiftlint:disable:next line_length function_body_length
132     private class func createPersistentStoreCoordinator(_ info: CoreDataIntormation, _ model: NSManagedObjectModel)
133         throws -> NSPersistentStoreCoordinator {
134         var failError: NSError? = nil
135         var shouldFail = false
136         var failureReason = "There was an error creating or loading the application's saved data."
137         
138         do {
139             let p = try ApplicationDirecrories.support.resourceValues(forKeys: [.isDirectoryKey])
140             if !p.isDirectory! {
141                 // swiftlint:disable:next line_length
142                 failureReason = "Expected a folder to store application data, found a file \(ApplicationDirecrories.support.path)."
143                 shouldFail = true
144             }
145         } catch {
146             let nserror = error as NSError
147             if nserror.code == NSFileReadNoSuchFileError {
148                 do {
149                     try FileManager
150                         .default
151                         .createDirectory(at: ApplicationDirecrories.support,
152                                          withIntermediateDirectories: false,
153                                          attributes: nil)
154                 } catch {
155                     failError = nserror
156                 }
157             } else {
158                 failError = nserror
159             }
160         }
161         
162         var coordinator: NSPersistentStoreCoordinator? = nil
163         if failError == nil {
164             coordinator = NSPersistentStoreCoordinator(managedObjectModel: model)
165             let url = ApplicationDirecrories.support.appendingPathComponent(info.fileName)
166             do {
167                 try coordinator!.addPersistentStore(ofType: info.type,
168                                                     configurationName: nil,
169                                                     at: url,
170                                                     options: info.options)
171             } catch {
172                 failError = error as NSError
173                 
174                 // Data Modelが更新されていたらストアファイルを削除してもう一度
175                 if failError?.domain == NSCocoaErrorDomain,
176                     (failError?.code == 134130 || failError?.code == 134110),
177                     info.tryRemake {
178                     self.removeDataFile(info)
179                     do {
180                         try coordinator!.addPersistentStore(ofType: info.type,
181                                                             configurationName: nil,
182                                                             at: url,
183                                                             options: info.options)
184                         failError = nil
185                     } catch {
186                         failError = error as NSError
187                     }
188                 }
189             }
190         }
191         
192         if shouldFail || (failError != nil) {
193             if let error = failError {
194                 NSApplication.shared().presentError(error)
195             }
196             throw CoreDataError.couldNotCreateCoordinator(failureReason)
197         }
198         return coordinator!
199     }
200     // swiftlint:disable:next line_length
201     private class func createManagedObjectContext(_ coordinator: NSPersistentStoreCoordinator) -> NSManagedObjectContext {
202         let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
203         moc.persistentStoreCoordinator = coordinator
204         moc.undoManager = nil
205         return moc
206     }
207     private class func removeDataFile(_ info: CoreDataIntormation) {
208         CoreDataRemover.remove(name: info.fileName)
209     }
210 }
211
212 extension CoreDataManager where Self: CoreDataProvider {
213     func removeDataFile() {
214         CoreDataRemover.remove(name: self.core.info.fileName)
215     }
216 }
217
218 extension CoreDataProvider {
219     func save() {
220         if !context.commitEditing() {
221             NSLog("\(String(describing: type(of: self))) unable to commit editing before saveing")
222             return
223         }
224         do {
225             try context.save()
226         } catch { presentOnMainThread(error) }
227         if let p = context.parent {
228             p.performAndWait {
229                 do {
230                     try p.save()
231                 } catch { self.presentOnMainThread(error) }
232             }
233         }
234     }
235     private func presentOnMainThread(_ error: Error) {
236         if Thread.isMainThread {
237             NSApp.presentError(error)
238         } else {
239             DispatchQueue.main.sync {
240                 let _ = NSApp.presentError(error)
241             }
242         }
243     }
244 }
245
246 extension CoreDataAccessor {
247     func insertNewObject<T>(for entity: Entity<T>) -> T? {
248         return NSEntityDescription
249             .insertNewObject(forEntityName: entity.name,
250                              into: context) as? T
251     }
252     func delete(_ object: NSManagedObject) {
253         context.delete(object)
254     }
255     func object(with objectId: NSManagedObjectID) -> NSManagedObject {
256         return context.object(with: objectId)
257     }
258     func objects<T>(with entity: Entity<T>,
259                     sortDescriptors: [NSSortDescriptor]? = nil,
260                     predicate: NSPredicate? = nil) throws -> [T] {
261         let req = NSFetchRequest<T>(entityName: entity.name)
262         req.sortDescriptors = sortDescriptors
263         req.predicate = predicate
264         return try context.fetch(req)
265     }
266 }