Files
RestKit/Code/CoreData/RKManagedObjectStore.m

491 lines
22 KiB
Objective-C

//
// RKManagedObjectStore.m
// RestKit
//
// Created by Blake Watters on 9/22/09.
// Copyright 2009 Two Toasters
//
// 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 "RKManagedObjectStore.h"
#import "NSManagedObject+ActiveRecord.h"
#import "RKLog.h"
#import "RKSearchWordObserver.h"
#import "RKObjectPropertyInspector.h"
#import "RKObjectPropertyInspector+CoreData.h"
#import "RKAlert.h"
#import "RKLog.h"
#import "RKDirectory.h"
// Set Logging Component
#undef RKLogComponent
#define RKLogComponent lcl_cRestKitCoreData
NSString* const RKManagedObjectStoreDidFailSaveNotification = @"RKManagedObjectStoreDidFailSaveNotification";
static NSString* const RKManagedObjectStoreThreadDictionaryContextKey = @"RKManagedObjectStoreThreadDictionaryContextKey";
static NSString* const RKManagedObjectStoreThreadDictionaryEntityCacheKey = @"RKManagedObjectStoreThreadDictionaryEntityCacheKey";
@interface RKManagedObjectStore (Private)
- (id)initWithStoreFilename:(NSString *)storeFilename inDirectory:(NSString *)nilOrDirectoryPath usingSeedDatabaseName:(NSString *)nilOrNameOfSeedDatabaseInMainBundle managedObjectModel:(NSManagedObjectModel*)nilOrManagedObjectModel delegate:(id)delegate;
- (void)createPersistentStoreCoordinator;
- (void)createStoreIfNecessaryUsingSeedDatabase:(NSString*)seedDatabase;
- (NSManagedObjectContext*)newManagedObjectContext;
@end
@implementation RKManagedObjectStore
@synthesize delegate = _delegate;
@synthesize storeFilename = _storeFilename;
@synthesize pathToStoreFile = _pathToStoreFile;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;
@synthesize managedObjectCache = _managedObjectCache;
+ (RKManagedObjectStore*)objectStoreWithStoreFilename:(NSString*)storeFilename {
return [self objectStoreWithStoreFilename:storeFilename usingSeedDatabaseName:nil managedObjectModel:nil delegate:nil];
}
+ (RKManagedObjectStore*)objectStoreWithStoreFilename:(NSString *)storeFilename usingSeedDatabaseName:(NSString *)nilOrNameOfSeedDatabaseInMainBundle managedObjectModel:(NSManagedObjectModel*)nilOrManagedObjectModel delegate:(id)delegate {
return [[[self alloc] initWithStoreFilename:storeFilename inDirectory:nil usingSeedDatabaseName:nilOrNameOfSeedDatabaseInMainBundle managedObjectModel:nilOrManagedObjectModel delegate:delegate] autorelease];
}
+ (RKManagedObjectStore*)objectStoreWithStoreFilename:(NSString *)storeFilename inDirectory:(NSString *)directory usingSeedDatabaseName:(NSString *)nilOrNameOfSeedDatabaseInMainBundle managedObjectModel:(NSManagedObjectModel*)nilOrManagedObjectModel delegate:(id)delegate {
return [[[self alloc] initWithStoreFilename:storeFilename inDirectory:directory usingSeedDatabaseName:nilOrNameOfSeedDatabaseInMainBundle managedObjectModel:nilOrManagedObjectModel delegate:delegate] autorelease];
}
- (id)initWithStoreFilename:(NSString*)storeFilename {
return [self initWithStoreFilename:storeFilename inDirectory:nil usingSeedDatabaseName:nil managedObjectModel:nil delegate:nil];
}
- (id)initWithStoreFilename:(NSString *)storeFilename inDirectory:(NSString *)nilOrDirectoryPath usingSeedDatabaseName:(NSString *)nilOrNameOfSeedDatabaseInMainBundle managedObjectModel:(NSManagedObjectModel*)nilOrManagedObjectModel delegate:(id)delegate {
self = [self init];
if (self) {
_storeFilename = [storeFilename retain];
if (nilOrDirectoryPath == nil) {
nilOrDirectoryPath = [RKDirectory applicationDataDirectory];
} else {
BOOL isDir;
NSAssert1([[NSFileManager defaultManager] fileExistsAtPath:nilOrDirectoryPath isDirectory:&isDir] && isDir == YES, @"Specified storage directory exists", nilOrDirectoryPath);
}
_pathToStoreFile = [[nilOrDirectoryPath stringByAppendingPathComponent:_storeFilename] retain];
if (nilOrManagedObjectModel == nil) {
// NOTE: allBundles permits Core Data setup in unit tests
nilOrManagedObjectModel = [NSManagedObjectModel mergedModelFromBundles:[NSBundle allBundles]];
}
NSMutableArray* allManagedObjectModels = [[NSMutableArray alloc] init];
[allManagedObjectModels addObject:nilOrManagedObjectModel];
NSString* rkBundlePath = [[NSBundle mainBundle] pathForResource:@"RestKitResources"
ofType:@"bundle"];
NSURL* rkCoreDataLibraryMOMURL = [[NSBundle bundleWithPath:rkBundlePath] URLForResource:@"RestKitCoreData"
withExtension:@"momd"];
NSManagedObjectModel* rkCoreDataLibraryMOM = [[NSManagedObjectModel alloc] initWithContentsOfURL:rkCoreDataLibraryMOMURL];
if (rkCoreDataLibraryMOM) {
[allManagedObjectModels addObject:rkCoreDataLibraryMOM];
[rkCoreDataLibraryMOM release];
} else {
RKLogWarning(@"Unable to find RestKitCoreData.momd within the RestKitCoreDataBundle.bundle");
}
_managedObjectModel = [[NSManagedObjectModel modelByMergingModels:allManagedObjectModels] retain];
[allManagedObjectModels release];
if (nilOrNameOfSeedDatabaseInMainBundle) {
[self createStoreIfNecessaryUsingSeedDatabase:nilOrNameOfSeedDatabaseInMainBundle];
}
_delegate = delegate;
[self createPersistentStoreCoordinator];
// Ensure there is a search word observer
[RKSearchWordObserver sharedObserver];
}
return self;
}
- (void)setThreadLocalObject:(id)value forKey:(id)key {
NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary];
NSString *objectStoreKey = [NSString stringWithFormat:@"RKManagedObjectStore_%p", self];
if (! [threadDictionary valueForKey:objectStoreKey]) {
[threadDictionary setValue:[NSMutableDictionary dictionary] forKey:objectStoreKey];
}
[[threadDictionary objectForKey:objectStoreKey] setObject:value forKey:key];
}
- (id)threadLocalObjectForKey:(id)key {
NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary];
NSString *objectStoreKey = [NSString stringWithFormat:@"RKManagedObjectStore_%p", self];
if (! [threadDictionary valueForKey:objectStoreKey]) {
[threadDictionary setObject:[NSMutableDictionary dictionary] forKey:objectStoreKey];
}
return [[threadDictionary objectForKey:objectStoreKey] objectForKey:key];
}
- (void)removeThreadLocalObjectForKey:(id)key {
NSMutableDictionary* threadDictionary = [[NSThread currentThread] threadDictionary];
NSString *objectStoreKey = [NSString stringWithFormat:@"RKManagedObjectStore_%p", self];
if (! [threadDictionary valueForKey:objectStoreKey]) {
[threadDictionary setObject:[NSMutableDictionary dictionary] forKey:objectStoreKey];
}
[[threadDictionary objectForKey:objectStoreKey] removeObjectForKey:key];
}
- (void)clearThreadLocalStorage {
// Clear out our Thread local information
NSManagedObjectContext *managedObjectContext = [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey];
if (managedObjectContext) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextObjectsDidChangeNotification object:managedObjectContext];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:managedObjectContext];
[self removeThreadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey];
}
if ([self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]) {
[self removeThreadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey];
}
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self clearThreadLocalStorage];
[_storeFilename release];
_storeFilename = nil;
[_pathToStoreFile release];
_pathToStoreFile = nil;
[_managedObjectModel release];
_managedObjectModel = nil;
[_persistentStoreCoordinator release];
_persistentStoreCoordinator = nil;
[_managedObjectCache release];
_managedObjectCache = nil;
[super dealloc];
}
/**
Performs the save action for the application, which is to send the save:
message to the application's managed object context.
*/
- (NSError*)save {
NSManagedObjectContext* moc = [self managedObjectContext];
NSError *error = nil;
@try {
if (![moc save:&error]) {
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToSaveContext:error:exception:)]) {
[self.delegate managedObjectStore:self didFailToSaveContext:moc error:error exception:nil];
}
NSDictionary* userInfo = [NSDictionary dictionaryWithObject:error forKey:@"error"];
[[NSNotificationCenter defaultCenter] postNotificationName:RKManagedObjectStoreDidFailSaveNotification object:self userInfo:userInfo];
if ([[error domain] isEqualToString:@"NSCocoaErrorDomain"]) {
NSDictionary *userInfo = [error userInfo];
NSArray *errors = [userInfo valueForKey:@"NSDetailedErrors"];
if (errors) {
for (NSError *detailedError in errors) {
NSDictionary *subUserInfo = [detailedError userInfo];
RKLogError(@"Core Data Save Error\n \
NSLocalizedDescription:\t\t%@\n \
NSValidationErrorKey:\t\t\t%@\n \
NSValidationErrorPredicate:\t%@\n \
NSValidationErrorObject:\n%@\n",
[subUserInfo valueForKey:@"NSLocalizedDescription"],
[subUserInfo valueForKey:@"NSValidationErrorKey"],
[subUserInfo valueForKey:@"NSValidationErrorPredicate"],
[subUserInfo valueForKey:@"NSValidationErrorObject"]);
}
}
else {
RKLogError(@"Core Data Save Error\n \
NSLocalizedDescription:\t\t%@\n \
NSValidationErrorKey:\t\t\t%@\n \
NSValidationErrorPredicate:\t%@\n \
NSValidationErrorObject:\n%@\n",
[userInfo valueForKey:@"NSLocalizedDescription"],
[userInfo valueForKey:@"NSValidationErrorKey"],
[userInfo valueForKey:@"NSValidationErrorPredicate"],
[userInfo valueForKey:@"NSValidationErrorObject"]);
}
}
return error;
}
}
@catch (NSException* e) {
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToSaveContext:error:exception:)]) {
[self.delegate managedObjectStore:self didFailToSaveContext:moc error:nil exception:e];
}
else {
@throw;
}
}
return nil;
}
- (NSManagedObjectContext*)newManagedObjectContext {
NSManagedObjectContext* managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
[managedObjectContext setUndoManager:nil];
[managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(objectsDidChange:)
name:NSManagedObjectContextObjectsDidChangeNotification
object:managedObjectContext];
return managedObjectContext;
}
- (void)createStoreIfNecessaryUsingSeedDatabase:(NSString*)seedDatabase {
if (NO == [[NSFileManager defaultManager] fileExistsAtPath:self.pathToStoreFile]) {
NSString* seedDatabasePath = [[NSBundle mainBundle] pathForResource:seedDatabase ofType:nil];
NSAssert1(seedDatabasePath, @"Unable to find seed database file '%@' in the Main Bundle, aborting...", seedDatabase);
RKLogInfo(@"No existing database found, copying from seed path '%@'", seedDatabasePath);
NSError* error;
if (![[NSFileManager defaultManager] copyItemAtPath:seedDatabasePath toPath:self.pathToStoreFile error:&error]) {
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToCopySeedDatabase:error:)]) {
[self.delegate managedObjectStore:self didFailToCopySeedDatabase:seedDatabase error:error];
} else {
RKLogError(@"Encountered an error during seed database copy: %@", [error localizedDescription]);
}
}
NSAssert1([[NSFileManager defaultManager] fileExistsAtPath:seedDatabasePath], @"Seed database not found at path '%@'!", seedDatabasePath);
}
}
- (void)createPersistentStoreCoordinator {
NSAssert(_managedObjectModel, @"Cannot create persistent store coordinator without a managed object model");
NSAssert(!_persistentStoreCoordinator, @"Cannot create persistent store coordinator: one already exists.");
NSURL *storeUrl = [NSURL fileURLWithPath:self.pathToStoreFile];
NSError *error;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_managedObjectModel];
// Allow inferred migration from the original version of the application.
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error]) {
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToCreatePersistentStoreCoordinatorWithError:)]) {
[self.delegate managedObjectStore:self didFailToCreatePersistentStoreCoordinatorWithError:error];
} else {
NSAssert(NO, @"Managed object store failed to create persistent store coordinator: %@", error);
}
}
}
- (void)deletePersistantStoreUsingSeedDatabaseName:(NSString *)seedFile {
NSURL* storeURL = [NSURL fileURLWithPath:self.pathToStoreFile];
NSError* error = nil;
if ([[NSFileManager defaultManager] fileExistsAtPath:storeURL.path]) {
if (![[NSFileManager defaultManager] removeItemAtPath:storeURL.path error:&error]) {
if (self.delegate != nil && [self.delegate respondsToSelector:@selector(managedObjectStore:didFailToDeletePersistentStore:error:)]) {
[self.delegate managedObjectStore:self didFailToDeletePersistentStore:self.pathToStoreFile error:error];
}
else {
NSAssert(NO, @"Managed object store failed to delete persistent store : %@", error);
}
}
} else {
RKLogWarning(@"Asked to delete persistent store but no store file exists at path: %@", storeURL.path);
}
[_persistentStoreCoordinator release];
_persistentStoreCoordinator = nil;
if (seedFile) {
[self createStoreIfNecessaryUsingSeedDatabase:seedFile];
}
[self createPersistentStoreCoordinator];
}
- (void)deletePersistantStore {
[self deletePersistantStoreUsingSeedDatabaseName:nil];
}
/**
*
* Override managedObjectContext getter to ensure we return a separate context
* for each NSThread.
*
*/
-(NSManagedObjectContext*)managedObjectContext {
NSManagedObjectContext* managedObjectContext = [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryContextKey];
if (!managedObjectContext) {
managedObjectContext = [self newManagedObjectContext];
// Store into thread local storage dictionary
[self setThreadLocalObject:managedObjectContext forKey:RKManagedObjectStoreThreadDictionaryContextKey];
[managedObjectContext release];
// If we are a background Thread MOC, we need to inform the main thread on save
if (![NSThread isMainThread]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:managedObjectContext];
}
}
return managedObjectContext;
}
- (void)mergeChangesOnMainThreadWithNotification:(NSNotification*)notification {
assert([NSThread isMainThread]);
[self.managedObjectContext performSelectorOnMainThread:@selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
- (void)mergeChanges:(NSNotification *)notification {
// Merge changes into the main context on the main thread
[self performSelectorOnMainThread:@selector(mergeChangesOnMainThreadWithNotification:) withObject:notification waitUntilDone:YES];
}
- (BOOL)shouldCoerceAttributeToString:(NSString *)attribute forEntity:(NSEntityDescription *)entity {
Class attributeType = [[RKObjectPropertyInspector sharedInspector] typeForProperty:attribute ofEntity:entity];
return [attributeType instancesRespondToSelector:@selector(stringValue)];
}
- (void)objectsDidChange:(NSNotification*)notification {
NSDictionary* userInfo = notification.userInfo;
NSSet* insertedObjects = [userInfo objectForKey:NSInsertedObjectsKey];
NSMutableDictionary* entityCache = [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey];
for (NSManagedObject* object in insertedObjects) {
if ([object respondsToSelector:@selector(primaryKeyProperty)]) {
Class theClass = [object class];
NSString* primaryKey = [theClass performSelector:@selector(primaryKeyProperty)];
id primaryKeyValue = [object valueForKey:primaryKey];
// TODO: Unit test that this is coerced into a string!!
NSEntityDescription *entity = [(NSManagedObject *)object entity];
if ([self shouldCoerceAttributeToString:primaryKey forEntity:entity]) {
primaryKeyValue = [primaryKeyValue stringValue];
}
NSMutableDictionary* classCache = [entityCache objectForKey:entity.name];
if (classCache && primaryKeyValue && [classCache objectForKey:primaryKeyValue] == nil) {
[classCache setObject:object forKey:primaryKeyValue];
}
}
}
}
#pragma mark -
#pragma mark Helpers
- (NSManagedObject*)objectWithID:(NSManagedObjectID*)objectID {
NSAssert(objectID, @"Cannot fetch a managedObject with a nil objectID");
return [self.managedObjectContext objectWithID:objectID];
}
- (NSArray*)objectsWithIDs:(NSArray*)objectIDs {
NSMutableArray* objects = [[NSMutableArray alloc] init];
for (NSManagedObjectID* objectID in objectIDs) {
[objects addObject:[self objectWithID:objectID]];
}
NSArray* objectArray = [NSArray arrayWithArray:objects];
[objects release];
return objectArray;
}
- (NSManagedObject*)findOrCreateInstanceOfEntity:(NSEntityDescription*)entity withPrimaryKeyAttribute:(NSString*)primaryKeyAttribute andValue:(id)primaryKeyValue {
NSAssert(entity, @"Cannot instantiate managed object without a target class");
NSAssert(primaryKeyAttribute, @"Cannot find existing managed object instance without a primary key attribute");
NSAssert(primaryKeyValue, @"Cannot find existing managed object by primary key without a value");
NSManagedObject* object = nil;
// NOTE: We coerce the primary key into a string (if possible) for convenience. Generally
// primary keys are expressed either as a number of a string, so this lets us support either case interchangeably
id lookupValue = [primaryKeyValue respondsToSelector:@selector(stringValue)] ? [primaryKeyValue stringValue] : primaryKeyValue;
NSArray* objectIds = nil;
NSString* entityName = entity.name;
if (nil == [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey]) {
[self setThreadLocalObject:[NSMutableDictionary dictionary] forKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey];
}
// Construct the cache if necessary
NSMutableDictionary* entityCache = [self threadLocalObjectForKey:RKManagedObjectStoreThreadDictionaryEntityCacheKey];
if (nil == [entityCache objectForKey:entityName]) {
NSFetchRequest* fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:entity];
[fetchRequest setResultType:NSManagedObjectIDResultType];
objectIds = [NSManagedObject executeFetchRequest:fetchRequest];
RKLogInfo(@"Caching all %d %@ objectsIDs to thread local storage", [objectIds count], entity.name);
NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];
if ([objectIds count] > 0) {
BOOL coerceToString = [self shouldCoerceAttributeToString:primaryKeyAttribute forEntity:entity];
for (NSManagedObjectID* theObjectID in objectIds) {
NSManagedObject* theObject = [self objectWithID:theObjectID];
id attributeValue = [theObject valueForKey:primaryKeyAttribute];
// Coerce to a string if possible
attributeValue = coerceToString ? [attributeValue stringValue] : attributeValue;
if (attributeValue) {
[dictionary setObject:theObjectID forKey:attributeValue];
}
}
}
[entityCache setObject:dictionary forKey:entityName];
}
NSMutableDictionary* dictionary = [entityCache objectForKey:entityName];
NSAssert1(dictionary, @"Thread local cache of %@ objectIDs should not be nil", entityName);
NSManagedObjectID* objectId = [dictionary objectForKey:lookupValue];
if (objectId == nil) {
object = [[[NSManagedObject alloc] initWithEntity:entity insertIntoManagedObjectContext:self.managedObjectContext] autorelease];
NSError* error = nil;
BOOL success = [self.managedObjectContext obtainPermanentIDsForObjects:[NSArray arrayWithObject:object]
error:&error];
if (success) {
[dictionary setObject:[object objectID] forKey:lookupValue];
} else {
RKLogError(@"Object store %@ was unable to obtain a permanent object ID for %@", self, object);
}
} else {
object = [self objectWithID:objectId];
}
NSAssert(object, @"Object should not be nil");
return object;
}
- (NSArray*)objectsForResourcePath:(NSString *)resourcePath {
NSArray* cachedObjects = nil;
if (self.managedObjectCache) {
NSFetchRequest* cacheFetchRequest = [self.managedObjectCache fetchRequestForResourcePath:resourcePath];
if (cacheFetchRequest) {
cachedObjects = [NSManagedObject objectsWithFetchRequest:cacheFetchRequest];
} else {
RKLogDebug(@"Failed to find a fetchRequest for resourcePath: %@", resourcePath);
}
}
return cachedObjects;
}
@end