Rename Core Data objects.

URLPreviewCacheData becomes URLPreviewData in the model with a class name of URLPreviewDataMO
ClosedURLData becomes URLPreviewUserData in the model with a class name of URLPreviewUserDataMO
This commit is contained in:
Doug
2021-09-08 15:47:14 +01:00
parent ecf9503dc9
commit 173b22822c
5 changed files with 28 additions and 25 deletions
@@ -0,0 +1,48 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreData
extension URLPreviewDataMO {
convenience init(context: NSManagedObjectContext, preview: URLPreviewData, creationDate: Date) {
self.init(context: context)
update(from: preview, on: creationDate)
}
func update(from preview: URLPreviewData, on date: Date) {
url = preview.url
siteName = preview.siteName
title = preview.title
text = preview.text
image = preview.image
creationDate = date
}
func preview(for event: MXEvent) -> URLPreviewData? {
guard let url = url else { return nil }
let viewData = URLPreviewData(url: url,
eventID: event.eventId,
roomID: event.roomId,
siteName: siteName,
title: title,
text: text)
viewData.image = image as? UIImage
return viewData
}
}
@@ -0,0 +1,45 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreData
/// A `ValueTransformer` for ``URLPreviewCacheData``'s `image` field.
/// This class transforms between `UIImage` and it's `pngData()` representation.
class URLPreviewImageTransformer: ValueTransformer {
override class func transformedValueClass() -> AnyClass {
UIImage.self
}
override class func allowsReverseTransformation() -> Bool {
true
}
/// Transforms a `UIImage` into it's `pngData()` representation.
override func transformedValue(_ value: Any?) -> Any? {
guard let image = value as? UIImage else { return nil }
return image.pngData()
}
/// Transforms `Data` into a `UIImage`
override func reverseTransformedValue(_ value: Any?) -> Any? {
guard let data = value as? Data else { return nil }
return UIImage(data: data)
}
}
extension NSValueTransformerName {
static let urlPreviewImageTransformer = NSValueTransformerName("URLPreviewImageTransformer")
}
@@ -0,0 +1,165 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreData
/// A cache for URL previews backed by Core Data.
class URLPreviewStore {
// MARK: - Properties
/// The Core Data container for persisting the cache to disk.
private let container: NSPersistentContainer
/// The Core Data context used to store and load data on.
private var context: NSManagedObjectContext {
container.viewContext
}
/// A time interval that represents how long an item in the cache is valid for.
private let dataValidityTime: TimeInterval = 60 * 60 * 24
/// The oldest `creationDate` allowed for valid data.
private var expiryDate: Date {
Date().addingTimeInterval(-dataValidityTime)
}
// MARK: - Lifecycle
/// Create a URLPreview Cache optionally storing the data in memory.
/// - Parameter inMemory: Whether to store the data in memory.
init(inMemory: Bool = false) {
// Register the transformer for the `image` field.
ValueTransformer.setValueTransformer(URLPreviewImageTransformer(), forName: .urlPreviewImageTransformer)
// Create the container, updating it's path if storing the data in memory.
container = NSPersistentContainer(name: "URLPreviewStore")
if inMemory {
if let storeDescription = container.persistentStoreDescriptions.first {
storeDescription.url = URL(fileURLWithPath: "/dev/null")
} else {
MXLog.error("[URLPreviewStore] persistentStoreDescription not found.")
}
}
// Load the persistent stores into the container
container.loadPersistentStores { storeDescription, error in
if let error = error {
MXLog.error("[URLPreviewStore] Core Data container error: \(error.localizedDescription)")
}
}
}
// MARK: - Public
/// Cache a preview in the store. If a preview already exists with the same URL it will be updated from the new preview.
/// - Parameter preview: The preview to add to the store.
/// - Parameter date: Optional: The date the preview was generated. When nil, the current date is assigned.
func cache(_ preview: URLPreviewData, generatedOn generationDate: Date? = nil) {
// Create a fetch request for an existing preview.
let request: NSFetchRequest<URLPreviewDataMO> = URLPreviewDataMO.fetchRequest()
request.predicate = NSPredicate(format: "url == %@", preview.url as NSURL)
// Use the custom date if supplied (currently this is for testing purposes)
let date = generationDate ?? Date()
// Update existing data if found otherwise create new data.
if let cachedPreview = try? context.fetch(request).first {
cachedPreview.update(from: preview, on: date)
} else {
_ = URLPreviewDataMO(context: context, preview: preview, creationDate: date)
}
save()
}
/// Fetches the preview from the cache for the supplied URL. If a preview doesn't exist or
/// if the preview is older than the ``dataValidityTime`` the returned value will be nil.
/// - Parameter url: The URL to fetch the preview for.
/// - Returns: The preview if found, otherwise nil.
func preview(for url: URL, and event: MXEvent) -> URLPreviewData? {
// Create a request for the url excluding any expired items
let request: NSFetchRequest<URLPreviewDataMO> = URLPreviewDataMO.fetchRequest()
request.predicate = NSCompoundPredicate(type: .and, subpredicates: [
NSPredicate(format: "url == %@", url as NSURL),
NSPredicate(format: "creationDate > %@", expiryDate as NSDate)
])
// Fetch the request, returning nil if nothing was found
guard
let cachedPreview = try? context.fetch(request).first
else { return nil }
// Convert and return
return cachedPreview.preview(for: event)
}
/// Returns the number of URL previews cached in the store.
func cacheCount() -> Int {
let request: NSFetchRequest<NSFetchRequestResult> = URLPreviewDataMO.fetchRequest()
return (try? context.count(for: request)) ?? 0
}
/// Removes any expired cache data from the store.
func removeExpiredItems() {
let request: NSFetchRequest<NSFetchRequestResult> = URLPreviewDataMO.fetchRequest()
request.predicate = NSPredicate(format: "creationDate < %@", expiryDate as NSDate)
do {
try context.execute(NSBatchDeleteRequest(fetchRequest: request))
} catch {
MXLog.error("[URLPreviewStore] Error executing batch delete request: \(error.localizedDescription)")
}
}
/// Deletes all cache data and all closed previews from the store.
func deleteAll() {
do {
_ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewDataMO.fetchRequest()))
_ = try context.execute(NSBatchDeleteRequest(fetchRequest: URLPreviewUserDataMO.fetchRequest()))
} catch {
MXLog.error("[URLPreviewStore] Error executing batch delete request: \(error.localizedDescription)")
}
}
/// Store the dismissal of a preview from the event with `eventId` and `roomId`.
func closePreview(for eventId: String, in roomId: String) {
_ = URLPreviewUserDataMO(context: context, eventID: eventId, roomID: roomId, dismissed: true)
save()
}
/// Whether a preview for an event with the given `eventId` and `roomId` has been closed or not.
func hasClosedPreview(for eventId: String, in roomId: String) -> Bool {
// Create a request for the url excluding any expired items
let request: NSFetchRequest<URLPreviewUserDataMO> = URLPreviewUserDataMO.fetchRequest()
request.predicate = NSCompoundPredicate(type: .and, subpredicates: [
NSPredicate(format: "eventID == %@", eventId),
NSPredicate(format: "roomID == %@", roomId),
NSPredicate(format: "dismissed == true")
])
return (try? context.count(for: request)) ?? 0 > 0
}
// MARK: - Private
/// Saves any changes that are found on the context
private func save() {
guard context.hasChanges else { return }
try? context.save()
}
}
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20G95" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="URLPreviewData" representedClassName="URLPreviewDataMO" syncable="YES" codeGenerationType="class">
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="image" optional="YES" attributeType="Transformable" valueTransformerName="URLPreviewImageTransformer"/>
<attribute name="siteName" optional="YES" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="title" optional="YES" attributeType="String"/>
<attribute name="url" attributeType="URI"/>
</entity>
<entity name="URLPreviewUserData" representedClassName="URLPreviewUserDataMO" syncable="YES" codeGenerationType="class">
<attribute name="dismissed" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="eventID" attributeType="String"/>
<attribute name="roomID" attributeType="String"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="eventID"/>
<constraint value="roomID"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<elements>
<element name="URLPreviewUserData" positionX="0" positionY="45" width="128" height="74"/>
<element name="URLPreviewData" positionX="160" positionY="192" width="128" height="119"/>
</elements>
</model>
@@ -0,0 +1,26 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import CoreData
extension URLPreviewUserDataMO {
convenience init(context: NSManagedObjectContext, eventID: String, roomID: String, dismissed: Bool) {
self.init(context: context)
self.eventID = eventID
self.roomID = roomID
self.dismissed = dismissed
}
}