// // KSExtensibleManagedObject.m // // Created by Mike Abdullah on 25/08/2007. // Copyright 2007-2008 Karelia Software. All rights reserved. // // THIS SOFTWARE IS PROVIDED BY KARELIA SOFTWARE AND ITS CONTRIBUTORS "AS-IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUR OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. // // // A special kind of managed object that allows you to use -valueForKey: and // -setValueForKey: using any key. If the object does not normally accept this // key, it is stored internally in a dictionary and then archived as data. #import "KSExtensibleManagedObject.h" @interface KSExtensibleManagedObject (Private) - (NSMutableDictionary *)_extensibleProperties; + (NSSet *)modifiedKeysBetweenDictionary:(NSDictionary *)dict1 andDictionary:(NSDictionary *)dict2; - (NSDictionary *)archivedExtensibleProperties; @end #pragma mark - @implementation KSExtensibleManagedObject #pragma mark - #pragma mark Class Methods + (NSString *)extensiblePropertiesDataKey { return @"extensiblePropertiesData"; } #pragma mark - #pragma mark Accessors - (NSDictionary *)extensibleProperties { NSDictionary *result = [NSDictionary dictionaryWithDictionary:[self _extensibleProperties]]; return result; } #pragma mark - #pragma mark Core Data /* Throw away our internal dictionary just like normal Core Data faulting behavior. */ - (void)didTurnIntoFault { [myExtensibleProperties release]; myExtensibleProperties = nil; [super didTurnIntoFault]; } - (NSMutableDictionary *)_extensibleProperties { // Fault in the properties on-demand if (!myExtensibleProperties) { myExtensibleProperties = [[self archivedExtensibleProperties] mutableCopy]; if (!myExtensibleProperties) { myExtensibleProperties = [[NSMutableDictionary alloc] init]; } } return myExtensibleProperties; } #pragma mark - #pragma mark KVC /* We catch all undefined keys and pull them from the extensible properties dictionary. */ - (id)valueForUndefinedKey:(NSString *)key { id result = [[self _extensibleProperties] valueForKey:key]; return result; } - (void) setValue: (id) value forExtensibleKey:(NSString *)key { [self willChangeValueForKey:key]; [[self _extensibleProperties] setValue:value forKey:key]; // Archive the new properties. This has to be done every time so Core Data knows // that some kind of change was made. [self setValue:[self archiveExtensibleProperties:[self _extensibleProperties]] forKey:[[self class] extensiblePropertiesDataKey]]; [self didChangeValueForKey:key]; } /* Undefined keys are caught and A) stored in-memory B) archived persistently */ - (void)setValue:(id)value forUndefinedKey:(NSString *)key { // Archive the new properties. This has to be done every time so Core Data knows // that some kind of change was made. [self setValue: value forExtensibleKey: key]; } /* Whenever a change to our dictionary data is made due to an undo or redo, match the changes to * our in-memory dictionary */ - (void)didChangeValueForKey:(NSString *)key { if ([key isEqualToString:[[self class] extensiblePropertiesDataKey]]) { NSUndoManager *undoManager = [[self managedObjectContext] undoManager]; if ([undoManager isUndoing] || [undoManager isRedoing]) { // Comparison of the old and new dictionaries in order to to send out approrpriate KVO notifications // We specifically access the ivar directly to avoid faulting it in. NSDictionary *replacementDictionary = [self archivedExtensibleProperties]; NSSet *modifiedKeys = [KSExtensibleManagedObject modifiedKeysBetweenDictionary:myExtensibleProperties andDictionary:replacementDictionary]; // Change each of the modified keys in our in-memory dictionary NSEnumerator *keysEnumerator = [modifiedKeys objectEnumerator]; NSString *aKey; while (aKey = [keysEnumerator nextObject]) { [self willChangeValueForKey:aKey]; [[self _extensibleProperties] setValue:[replacementDictionary valueForKey:aKey] forKey:aKey]; [self didChangeValueForKey:aKey]; } } } // Finally go ahead and do the default behavior. This is required to balance the // earlier -willChangeValueForKey: that must have ocurred. [super didChangeValueForKey:key]; } #pragma mark - #pragma mark Support + (NSSet *)modifiedKeysBetweenDictionary:(NSDictionary *)dict1 andDictionary:(NSDictionary *)dict2 { // It's easy if either dictionary is nil if (!dict1) return [NSSet setWithArray:[dict1 allKeys]]; if (!dict2) return [NSSet setWithArray:[dict2 allKeys]]; // Build the set containing all the keys that exist in either dictionary NSMutableSet *allKeys = [[NSMutableSet alloc] initWithArray:[dict1 allKeys]]; [allKeys addObjectsFromArray:[dict2 allKeys]]; // Then run through these building a list of keys which the two dictionaries have different values for NSEnumerator *enumerator = [allKeys objectEnumerator]; NSString *aKey; NSMutableSet *result = [NSMutableSet set]; while (aKey = [enumerator nextObject]) { if (![[dict1 valueForKey:aKey] isEqual:[dict2 valueForKey:aKey]]) { [result addObject:aKey]; } } // Tidy up [allKeys release]; return result; } /* Fetches all custom values from the persistent store rather than the in-memory representation. */ - (NSDictionary *)archivedExtensibleProperties { NSData *data = [self valueForKey:[[self class] extensiblePropertiesDataKey]]; NSDictionary *result = [self unarchiveExtensibleProperties:data]; return result; } - (NSDictionary *)unarchiveExtensibleProperties:(NSData *)propertiesData { NSMutableDictionary *result = nil; if (propertiesData) { id unarchivedDictionary = [NSKeyedUnarchiver unarchiveObjectWithData:propertiesData]; if ([unarchivedDictionary isKindOfClass:[NSMutableDictionary class]]) { result = unarchivedDictionary; } } return result; } - (NSData *)archiveExtensibleProperties:(NSDictionary *)properties; { NSData *result = [NSKeyedArchiver archivedDataWithRootObject:properties]; return result; } - (void) dealloc { [myExtensibleProperties release]; [super dealloc]; } @end