mirror of
https://github.com/zhigang1992/RestKit.git
synced 2026-05-06 17:32:52 +08:00
Implementation of Object Mapping 2.0 design:
* Removed RestKit from inheritance hierarchy * Mappings are implemented as concrete classes * Mapper is much more flexible & powerful * Much more robust error handling * Serialization is reimplemented as an object mapping operation * Added ability to serialize to JSON natively * Reworked Core Data integration * Simplified the codebase substantially
This commit is contained in:
@@ -2,581 +2,268 @@
|
||||
// RKObjectMapper.m
|
||||
// RestKit
|
||||
//
|
||||
// Created by Blake Watters on 3/4/10.
|
||||
// Copyright 2010 Two Toasters. All rights reserved.
|
||||
// Created by Blake Watters on 5/6/11.
|
||||
// Copyright 2011 Two Toasters. All rights reserved.
|
||||
//
|
||||
|
||||
#import <objc/message.h>
|
||||
|
||||
#import "../CoreData/CoreData.h"
|
||||
#import "RKObjectManager.h"
|
||||
|
||||
#import "RKObjectMapper.h"
|
||||
#import "NSDictionary+RKAdditions.h"
|
||||
#import "RKJSONParser.h"
|
||||
#import "RKXMLParser.h"
|
||||
#import "Errors.h"
|
||||
|
||||
// Default format string for date and time objects from Rails
|
||||
// TODO: Rails specifics should probably move elsewhere...
|
||||
static const NSString* kRKModelMapperRailsDateTimeFormatString = @"yyyy-MM-dd'T'HH:mm:ss'Z'"; // 2009-08-08T17:23:59Z
|
||||
static const NSString* kRKModelMapperRailsDateFormatString = @"MM/dd/yyyy";
|
||||
|
||||
@interface RKObjectMapper (Private)
|
||||
|
||||
- (id)parseString:(NSString*)string;
|
||||
|
||||
- (Class)typeClassForProperty:(NSString*)property ofClass:(Class)class;
|
||||
- (NSDictionary*)elementToPropertyMappingsForModel:(id)model;
|
||||
|
||||
- (NSObject<RKObjectMappable>*)findOrCreateInstanceOfModelClass:(Class)class fromElements:(NSDictionary*)elements;
|
||||
- (NSObject<RKObjectMappable>*)createOrUpdateInstanceOfModelClass:(Class)class fromElements:(NSDictionary*)elements;
|
||||
|
||||
- (void)updateModel:(id)model ifNewPropertyValue:(id)propertyValue forPropertyNamed:(NSString*)propertyName; // Rename!
|
||||
- (void)updateModel:(id)model fromElements:(NSDictionary*)elements;
|
||||
- (void)setPropertiesOfModel:(id)model fromElements:(NSDictionary*)elements;
|
||||
- (void)setRelationshipsOfModel:(id)object fromElements:(NSDictionary*)elements;
|
||||
|
||||
- (NSDate*)parseDateFromString:(NSString*)string;
|
||||
- (NSDate*)dateInLocalTime:(NSDate*)date;
|
||||
|
||||
@end
|
||||
#import "RKObjectMapperError.h"
|
||||
#import "RKObjectMapper_Private.h"
|
||||
|
||||
@implementation RKObjectMapper
|
||||
|
||||
@synthesize format = _format;
|
||||
@synthesize missingElementMappingPolicy = _missingElementMappingPolicy;
|
||||
@synthesize dateFormats = _dateFormats;
|
||||
@synthesize remoteTimeZone = _remoteTimeZone;
|
||||
@synthesize localTimeZone = _localTimeZone;
|
||||
@synthesize errorsKeyPath = _errorsKeyPath;
|
||||
@synthesize errorsConcatenationString = _errorsConcatenationString;
|
||||
@synthesize sourceObject = _sourceObject;
|
||||
@synthesize targetObject = _targetObject;
|
||||
@synthesize delegate =_delegate;
|
||||
@synthesize mappingProvider = _mappingProvider;
|
||||
@synthesize objectFactory = _objectFactory;
|
||||
@synthesize errors = _errors;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// public
|
||||
+ (id)mapperWithObject:(id)object mappingProvider:(RKObjectMappingProvider*)mappingProvider {
|
||||
return [[[self alloc] initWithObject:object mappingProvider:mappingProvider] autorelease];
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
if ((self = [super init])) {
|
||||
_elementToClassMappings = [[NSMutableDictionary alloc] init];
|
||||
_format = RKMappingFormatJSON;
|
||||
_missingElementMappingPolicy = RKIgnoreMissingElementMappingPolicy;
|
||||
_inspector = [[RKObjectPropertyInspector alloc] init];
|
||||
self.dateFormats = [NSArray arrayWithObjects:kRKModelMapperRailsDateTimeFormatString, kRKModelMapperRailsDateFormatString, nil];
|
||||
self.remoteTimeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
|
||||
self.localTimeZone = [NSTimeZone localTimeZone];
|
||||
self.errorsKeyPath = @"errors";
|
||||
self.errorsConcatenationString = @", ";
|
||||
}
|
||||
return self;
|
||||
- (id)initWithObject:(id)object mappingProvider:(RKObjectMappingProvider*)mappingProvider {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_sourceObject = [object retain];
|
||||
_mappingProvider = mappingProvider;
|
||||
_errors = [NSMutableArray new];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_elementToClassMappings release];
|
||||
[_inspector release];
|
||||
[_dateFormats release];
|
||||
[_errorsKeyPath release];
|
||||
[_errorsConcatenationString release];
|
||||
[super dealloc];
|
||||
[_sourceObject release];
|
||||
[_errors release];
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
- (void)registerClass:(Class<RKObjectMappable>)aClass forElementNamed:(NSString*)elementName {
|
||||
[_elementToClassMappings setObject:aClass forKey:elementName];
|
||||
#pragma mark - Errors
|
||||
|
||||
- (NSUInteger)errorCount {
|
||||
return [self.errors count];
|
||||
}
|
||||
|
||||
- (void)setFormat:(RKMappingFormat)format {
|
||||
_format = format;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Mapping from a string
|
||||
|
||||
- (id)parseString:(NSString*)string {
|
||||
Class parserClass;
|
||||
NSString* className = nil;
|
||||
NSObject<RKParser>* parser = nil;
|
||||
- (void)addError:(NSError*)error {
|
||||
NSAssert(error, @"Cannot add a nil error");
|
||||
[_errors addObject:error];
|
||||
|
||||
if (_format == RKMappingFormatJSON) {
|
||||
className = @"RKJSONParser";
|
||||
} else if (_format == RKMappingFormatXML) {
|
||||
className = @"RKXMLParser";
|
||||
if ([self.delegate respondsToSelector:@selector(objectMapper:didAddError:)]) {
|
||||
[self.delegate objectMapper:self didAddError:error];
|
||||
}
|
||||
|
||||
parserClass = NSClassFromString(className);
|
||||
if (nil == parserClass) {
|
||||
[NSException raise:@"Unable to find an appropriate parser."
|
||||
format:@"The object mapper attempted to process a payload via the '%@' parser, but it was not found.", className];
|
||||
}
|
||||
|
||||
parser = [[parserClass alloc] init];
|
||||
id result = nil;
|
||||
@try {
|
||||
result = [parser objectFromString:string];
|
||||
}
|
||||
@catch (NSException* e) {
|
||||
NSLog(@"[RestKit] RKObjectMapper:parseString: Exception (%@) parsing error from string: %@", [e reason], string);
|
||||
}
|
||||
[parser release];
|
||||
RKLOG_MAPPING(RKLogLevelError, @"Adding mapping error: %@", [error localizedDescription]);
|
||||
}
|
||||
|
||||
- (void)addErrorWithCode:(RKObjectMapperErrorCode)errorCode message:(NSString*)errorMessage keyPath:(NSString*)keyPath userInfo:(NSDictionary*)otherInfo {
|
||||
NSMutableDictionary* userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
|
||||
errorMessage, NSLocalizedDescriptionKey,
|
||||
@"RKObjectMapperKeyPath", keyPath ? keyPath : (NSString*) [NSNull null],
|
||||
nil];
|
||||
[userInfo addEntriesFromDictionary:otherInfo];
|
||||
NSError* error = [NSError errorWithDomain:RKRestKitErrorDomain code:errorCode userInfo:userInfo];
|
||||
[self addError:error];
|
||||
}
|
||||
|
||||
- (void)addErrorForUnmappableKeyPath:(NSString*)keyPath {
|
||||
NSString* errorMessage = [NSString stringWithFormat:@"Could not find an object mapping for keyPath: '%@'", keyPath];
|
||||
[self addErrorWithCode:RKObjectMapperErrorObjectMappingNotFound message:errorMessage keyPath:keyPath userInfo:nil];
|
||||
}
|
||||
|
||||
- (BOOL)isNullCollection:(id)object {
|
||||
// The purpose of this method is to guard against the case where we perform valueForKeyPath: on an array
|
||||
// and it returns NSNull for each element in the array.
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (NSError*)parseErrorFromString:(NSString*)string {
|
||||
NSString* errorMessage = [[[self parseString:string] valueForKeyPath:_errorsKeyPath] componentsJoinedByString:_errorsConcatenationString];
|
||||
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
errorMessage, NSLocalizedDescriptionKey,
|
||||
nil];
|
||||
NSError *error = [NSError errorWithDomain:RKRestKitErrorDomain code:RKObjectLoaderRemoteSystemError userInfo:userInfo];
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
// Primary entry point for RKObjectLoader
|
||||
- (id)mapFromString:(NSString*)string toClass:(Class<RKObjectMappable>)class keyPath:(NSString*)keyPath {
|
||||
id object = [self parseString:string];
|
||||
if (keyPath) {
|
||||
object = [object valueForKeyPath:keyPath];
|
||||
}
|
||||
|
||||
if ([object isKindOfClass:[NSDictionary class]]) {
|
||||
if (class) {
|
||||
return [self mapObjectFromDictionary:(NSDictionary*)object toClass:class];
|
||||
} else {
|
||||
return [self mapObjectFromDictionary:(NSDictionary*)object];
|
||||
}
|
||||
} else if ([object isKindOfClass:[NSArray class]]) {
|
||||
if (class) {
|
||||
return [self mapObjectsFromArrayOfDictionaries:(NSArray*)object toClass:class];
|
||||
} else {
|
||||
return [self mapObjectsFromArrayOfDictionaries:(NSArray*)object];
|
||||
}
|
||||
} else if (nil == object) {
|
||||
NSLog(@"[RestKit] RKModelMapper: mapObject:fromString: attempted to map from a nil payload. Skipping...");
|
||||
return nil;
|
||||
} else {
|
||||
[NSException raise:@"Unable to map from requested string"
|
||||
format:@"The object was deserialized into a %@. A dictionary or array of dictionaries was expected.", [object class]];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)mapFromString:(NSString*)string {
|
||||
return [self mapFromString:string toClass:nil keyPath:nil];
|
||||
}
|
||||
|
||||
- (void)mapObject:(NSObject<RKObjectMappable>*)model fromString:(NSString*)string keyPath:(NSString*)keyPath {
|
||||
id object = [self parseString:string];
|
||||
if (keyPath) {
|
||||
object = [object valueForKeyPath:keyPath];
|
||||
}
|
||||
|
||||
if ([object isKindOfClass:[NSDictionary class]]) {
|
||||
[self mapObject:model fromDictionary:object];
|
||||
} else if (nil == object) {
|
||||
NSLog(@"[RestKit] RKModelMapper: mapObject:fromString: attempted to map from a nil payload. Skipping...");
|
||||
return;
|
||||
} else {
|
||||
[NSException raise:@"Unable to map from requested string"
|
||||
format:@"The object was serialized into a %@. A dictionary of elements was expected. (Object: %@) [Payload: %@]", [object class], object, string];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mapObject:(NSObject<RKObjectMappable>*)model fromString:(NSString*)string {
|
||||
[self mapObject:model fromString:string keyPath:nil];
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Mapping from objects
|
||||
|
||||
- (void)mapObject:(NSObject<RKObjectMappable>*)model fromDictionary:(NSDictionary*)dictionary {
|
||||
Class class = [model class];
|
||||
|
||||
NSArray* elementNames = [_elementToClassMappings allKeysForObject:class];
|
||||
if ([elementNames count] == 0) {
|
||||
if ([model conformsToProtocol:@protocol(RKObjectMappable)]) {
|
||||
[self updateModel:model fromElements:dictionary];
|
||||
} else {
|
||||
[NSException raise:@"Unable to map from requested dictionary"
|
||||
format:@"There was no mappable element found for objects of type %@", class];
|
||||
}
|
||||
} else {
|
||||
for (NSString* elementName in elementNames) {
|
||||
if ([[dictionary allKeys] containsObject:elementName]) {
|
||||
NSDictionary* elements = [dictionary objectForKey:elementName];
|
||||
[self updateModel:model fromElements:elements];
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If the dictionary is not namespaced, attempt mapping its properties directly...
|
||||
[self updateModel:model fromElements:dictionary];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)mapObjectFromDictionary:(NSDictionary*)dictionary toClass:(Class)class {
|
||||
return [self createOrUpdateInstanceOfModelClass:class fromElements:dictionary];
|
||||
}
|
||||
|
||||
// Lookup an object type using the element to class mappings and map its attributes
|
||||
// Expects an object of the form {"registered_type": {"attribute1": "value", "attribute2": "value"}
|
||||
- (NSObject<RKObjectMappable>*)mapObjectFromDictionary:(NSDictionary*)dictionary {
|
||||
// NOTE: This will only map the first matching object in the dictionary will map
|
||||
// Here we have a dictionary that may contain a mappable object. We do not know the
|
||||
// type or keyPath the object or how many will match.
|
||||
// TODO: Move this logic into a more reusable place
|
||||
Class objectClass = nil;
|
||||
NSString* matchedKeyPath = nil;
|
||||
for (NSString* keyPath in [_elementToClassMappings allKeys]) {
|
||||
NSLog(@"Checking for keyPath %@ in %@", keyPath, dictionary);
|
||||
if ([dictionary valueForKeyPath:keyPath]) {
|
||||
if (objectClass) {
|
||||
// TODO: Factor out this log warning and use a self.logger so that we can unit test logging...
|
||||
NSLog(@"WARNING: Multiple mapping targets match in dictionary, using the first match: '%@' => [%@ class]. [Dictionary: %@]",
|
||||
matchedKeyPath, NSStringFromClass(objectClass), dictionary);
|
||||
} else {
|
||||
matchedKeyPath = [keyPath copy];
|
||||
objectClass = [_elementToClassMappings valueForKeyPath:keyPath];
|
||||
}
|
||||
// We consider an empty array/dictionary mappable, but a collection that contains only NSNull
|
||||
// values is unmappable
|
||||
if ([object respondsToSelector:@selector(objectForKey:)]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([object respondsToSelector:@selector(countForObject:)] && [object count] > 0) {
|
||||
if ([object countForObject:[NSNull null]] == [object count]) {
|
||||
RKLOG_MAPPING(RKLogLevelWarning, @"Found a collection containing only NSNull values, considering the collection unmappable...");
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
if (!objectClass) {
|
||||
NSLog(@"Unable to find registered mapping keyPath in payload, returning nil...");
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Mapping Primitives
|
||||
|
||||
- (id)mapObject:(id)mappableObject atKeyPath:(NSString*)keyPath usingMapping:(RKObjectMapping*)objectMapping {
|
||||
NSAssert([mappableObject respondsToSelector:@selector(setValue:forKeyPath:)], @"Expected self.object to be KVC compliant");
|
||||
id destinationObject = nil;
|
||||
|
||||
if (self.targetObject) {
|
||||
// If we find a mapping for this type and keyPath, map the entire dictionary to the target object
|
||||
destinationObject = self.targetObject;
|
||||
if (objectMapping && NO == [[self.targetObject class] isSubclassOfClass:objectMapping.objectClass]) {
|
||||
NSString* errorMessage = [NSString stringWithFormat:
|
||||
@"Expected an object mapping for class of type '%@', provider returned one for '%@'",
|
||||
NSStringFromClass([self.targetObject class]), NSStringFromClass(objectMapping.objectClass)];
|
||||
[self addErrorWithCode:RKObjectMapperErrorObjectMappingTypeMismatch message:errorMessage keyPath:keyPath userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
} else {
|
||||
destinationObject = [self objectWithMapping:objectMapping andData:mappableObject];
|
||||
}
|
||||
|
||||
if (objectMapping && destinationObject) {
|
||||
BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:objectMapping];
|
||||
if (success) {
|
||||
return destinationObject;
|
||||
}
|
||||
} else {
|
||||
// Attempted to map an object but couldn't find a mapping for the keyPath
|
||||
[self addErrorForUnmappableKeyPath:keyPath];
|
||||
return nil;
|
||||
}
|
||||
|
||||
id elements = [dictionary valueForKeyPath:matchedKeyPath];
|
||||
|
||||
// Support case where elements is an Array of objects
|
||||
id model = nil;
|
||||
if ([elements isKindOfClass:[NSDictionary class]]) {
|
||||
model = [self findOrCreateInstanceOfModelClass:objectClass fromElements:elements];
|
||||
[self updateModel:model fromElements:elements];
|
||||
} else if ([elements isKindOfClass:[NSArray class]]) {
|
||||
model = [self mapObjectsFromArrayOfDictionaries:elements toClass:objectClass];
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSArray*)mapCollection:(NSArray*)mappableObjects atKeyPath:(NSString*)keyPath usingMapping:(RKObjectMapping*)mapping {
|
||||
NSAssert(mappableObjects != nil, @"Cannot map without an collection of mappable objects");
|
||||
NSAssert(mapping != nil, @"Cannot map without a mapping to consult");
|
||||
|
||||
// Ensure we are mapping onto a mutable collection if there is a target
|
||||
NSMutableArray* mappedObjects = self.targetObject ? self.targetObject : [NSMutableArray arrayWithCapacity:[mappableObjects count]];
|
||||
if (NO == [mappedObjects respondsToSelector:@selector(addObject:)]) {
|
||||
NSString* errorMessage = [NSString stringWithFormat:
|
||||
@"Cannot map a collection of objects onto a non-mutable collection. Unexpected destination object type '%@'",
|
||||
NSStringFromClass([mappedObjects class])];
|
||||
[self addErrorWithCode:RKObjectMapperErrorObjectMappingTypeMismatch message:errorMessage keyPath:keyPath userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
- (NSArray*)mapObjectsFromArrayOfDictionaries:(NSArray*)array {
|
||||
NSMutableArray* objects = [NSMutableArray array];
|
||||
for (NSDictionary* dictionary in array) {
|
||||
if (![dictionary isKindOfClass:[NSNull class]]) {
|
||||
// TODO: Makes assumptions about the structure of the JSON...
|
||||
NSString* elementName = [[dictionary allKeys] objectAtIndex:0];
|
||||
Class class = [_elementToClassMappings valueForKeyPath:elementName];
|
||||
NSAssert(class != nil, @"Unable to perform object mapping without a destination class");
|
||||
NSDictionary* elements = [dictionary objectForKey:elementName];
|
||||
id object = [self createOrUpdateInstanceOfModelClass:class fromElements:elements];
|
||||
[objects addObject:object];
|
||||
}
|
||||
}
|
||||
|
||||
return (NSArray*)objects;
|
||||
}
|
||||
|
||||
- (NSDictionary*)mappableElementsForDictionary:(NSDictionary*)elements withObjectClass:(Class<RKObjectMappable>)objectClass {
|
||||
NSArray* elementNames = [_elementToClassMappings allKeysForObject:objectClass];
|
||||
if ([elementNames count] > 0) {
|
||||
for (NSString* elementName in elementNames) {
|
||||
if ([[elements allKeys] containsObject:elementName]) {
|
||||
return [elements objectForKey:elementName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assume that this dictionary is not namespaced and return it
|
||||
return elements;
|
||||
}
|
||||
|
||||
- (NSArray*)mappableElementsForArrayOfDictionaries:(NSArray*)dictionaries withObjectClass:(Class<RKObjectMappable>)objectClass {
|
||||
NSMutableArray* arrayOfElements = [NSMutableArray arrayWithCapacity:[dictionaries count]];
|
||||
for (NSDictionary* elements in dictionaries) {
|
||||
[arrayOfElements addObject:[self mappableElementsForDictionary:elements withObjectClass:objectClass]];
|
||||
for (id mappableObject in mappableObjects) {
|
||||
id destinationObject = [self objectWithMapping:mapping andData:mappableObject];
|
||||
BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping];
|
||||
if (success) {
|
||||
[mappedObjects addObject:destinationObject];
|
||||
}
|
||||
}
|
||||
|
||||
return arrayOfElements;
|
||||
return mappedObjects;
|
||||
}
|
||||
|
||||
- (NSArray*)mapObjectsFromArrayOfDictionaries:(NSArray*)array toClass:(Class<RKObjectMappable>)objectClass {
|
||||
NSMutableArray* objects = [NSMutableArray array];
|
||||
for (NSDictionary* elements in [self mappableElementsForArrayOfDictionaries:array withObjectClass:objectClass]) {
|
||||
if (![elements isKindOfClass:[NSNull class]]) {
|
||||
id object = [self createOrUpdateInstanceOfModelClass:objectClass fromElements:elements];
|
||||
[objects addObject:object];
|
||||
}
|
||||
}
|
||||
|
||||
return (NSArray*)objects;
|
||||
// The workhorse of this entire process. Emits object loading operations
|
||||
- (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPath:keyPath usingMapping:(RKObjectMapping*)mapping {
|
||||
NSAssert(destinationObject != nil, @"Cannot map without a target object to assign the results to");
|
||||
NSAssert(mappableObject != nil, @"Cannot map without a collection of attributes");
|
||||
NSAssert(mapping != nil, @"Cannot map without an mapping");
|
||||
|
||||
RKLOG_MAPPING(RKLogLevelDebug, @"Asked to map source object %@ with mapping %@", mappableObject, mapping);
|
||||
if ([self.delegate respondsToSelector:@selector(objectMapper:willMapFromObject:toObject:atKeyPath:usingMapping:)]) {
|
||||
[self.delegate objectMapper:self willMapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping];
|
||||
}
|
||||
|
||||
NSError* error = nil;
|
||||
RKObjectMappingOperation* operation = [RKObjectMappingOperation mappingOperationFromObject:mappableObject toObject:destinationObject withObjectMapping:mapping];
|
||||
operation.objectFactory = self;
|
||||
BOOL success = [operation performMapping:&error];
|
||||
if (success) {
|
||||
if ([self.delegate respondsToSelector:@selector(objectMapper:didMapFromObject:toObject:atKeyPath:usingMapping:)]) {
|
||||
[self.delegate objectMapper:self didMapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping];
|
||||
}
|
||||
} else {
|
||||
if ([self.delegate respondsToSelector:@selector(objectMapper:didFailMappingFromObject:toObject:withError:atKeyPath:usingMapping:)]) {
|
||||
[self.delegate objectMapper:self didFailMappingFromObject:mappableObject toObject:destinationObject withError:error atKeyPath:keyPath usingMapping:mapping];
|
||||
}
|
||||
[self addError:error];
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Utility Methods
|
||||
|
||||
- (Class)typeClassForProperty:(NSString*)property ofClass:(Class)class {
|
||||
return [[_inspector propertyNamesAndTypesForClass:class] objectForKey:property];
|
||||
}
|
||||
|
||||
- (NSDictionary*)elementToPropertyMappingsForModel:(id)model {
|
||||
return [[model class] elementToPropertyMappings];
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Persistent Instance Finders
|
||||
|
||||
// TODO: This version does not update properties. Should probably be realigned.
|
||||
- (NSObject<RKObjectMappable>*)findOrCreateInstanceOfModelClass:(Class)class fromElements:(NSDictionary*)elements {
|
||||
id object = nil;
|
||||
Class managedObjectClass = NSClassFromString(@"RKManagedObject");
|
||||
if (managedObjectClass && [class isSubclassOfClass:managedObjectClass]) {
|
||||
NSString* primaryKeyElement = [class performSelector:@selector(primaryKeyElement)];
|
||||
id primaryKeyValue = [elements objectForKey:primaryKeyElement];
|
||||
object = [[[RKObjectManager sharedManager] objectStore] findOrCreateInstanceOfManagedObject:class
|
||||
withPrimaryKeyValue:primaryKeyValue];
|
||||
}
|
||||
// instantiate if object is nil
|
||||
if (object == nil) {
|
||||
if ([class conformsToProtocol:@protocol(RKObjectMappable)] && [class respondsToSelector:@selector(object)]) {
|
||||
object = [class object];
|
||||
// Primary entry point for the mapper.
|
||||
- (RKObjectMappingResult*)performMapping {
|
||||
NSAssert(self.sourceObject != nil, @"Cannot perform object mapping without a source object to map from");
|
||||
NSAssert(self.mappingProvider != nil, @"Cannot perform object mapping without an object mapping provider");
|
||||
|
||||
RKLOG_MAPPING(RKLogLevelDebug, @"Performing object mapping sourceObject: %@\n and targetObject: %@", self.sourceObject, self.targetObject);
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(objectMapperWillBeginMapping:)]) {
|
||||
[self.delegate objectMapperWillBeginMapping:self];
|
||||
}
|
||||
|
||||
// Perform the mapping
|
||||
BOOL foundMappable = NO;
|
||||
NSMutableDictionary* results = [NSMutableDictionary dictionary];
|
||||
NSDictionary* keyPathsAndObjectMappings = [self.mappingProvider objectMappingsByKeyPath];
|
||||
for (NSString* keyPath in keyPathsAndObjectMappings) {
|
||||
id mappingResult;
|
||||
id mappableValue;
|
||||
|
||||
RKLOG_MAPPING(RKLogLevelInfo, @"Examining keyPath '%@' for mappable content...", keyPath);
|
||||
|
||||
if ([keyPath isEqualToString:@""]) {
|
||||
mappableValue = self.sourceObject;
|
||||
} else {
|
||||
// Allow non-RKObjectMappable objecs to pass through to alloc/init. Do we need this?
|
||||
object = [[[class alloc] init] autorelease];
|
||||
}
|
||||
}
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
- (NSObject<RKObjectMappable>*)createOrUpdateInstanceOfModelClass:(Class<RKObjectMappable>)class fromElements:(NSDictionary*)elements {
|
||||
id model = [self findOrCreateInstanceOfModelClass:class fromElements:elements];
|
||||
[self updateModel:model fromElements:elements];
|
||||
return model;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Property & Relationship Manipulation
|
||||
|
||||
- (void)updateModel:(NSObject<RKObjectMappable>*)model ifNewPropertyValue:(id)propertyValue forPropertyNamed:(NSString*)propertyName {
|
||||
id currentValue = [model valueForKey:propertyName];
|
||||
if (nil == currentValue && nil == propertyValue) {
|
||||
// Don't set the property, both are nil
|
||||
} else if (nil == propertyValue || [propertyValue isKindOfClass:[NSNull class]]) {
|
||||
// Clear out the value to reset it
|
||||
[model setValue:nil forKey:propertyName];
|
||||
} else if (currentValue == nil || [currentValue isKindOfClass:[NSNull class]]) {
|
||||
// Existing value was nil, just set the property and be happy
|
||||
[model setValue:propertyValue forKey:propertyName];
|
||||
} else {
|
||||
SEL comparisonSelector;
|
||||
if ([propertyValue isKindOfClass:[NSString class]]) {
|
||||
comparisonSelector = @selector(isEqualToString:);
|
||||
} else if ([propertyValue isKindOfClass:[NSNumber class]]) {
|
||||
comparisonSelector = @selector(isEqualToNumber:);
|
||||
} else if ([propertyValue isKindOfClass:[NSDate class]]) {
|
||||
comparisonSelector = @selector(isEqualToDate:);
|
||||
} else if ([propertyValue isKindOfClass:[NSArray class]]) {
|
||||
comparisonSelector = @selector(isEqualToArray:);
|
||||
} else if ([propertyValue isKindOfClass:[NSDictionary class]]) {
|
||||
comparisonSelector = @selector(isEqualToDictionary:);
|
||||
} else {
|
||||
[NSException raise:@"NoComparisonSelectorFound" format:@"You need a comparison selector for %@ (%@)", propertyName, [propertyValue class]];
|
||||
}
|
||||
|
||||
// Comparison magic using function pointers. See this page for details: http://www.red-sweater.com/blog/320/abusing-objective-c-with-class
|
||||
// Original code courtesy of Greg Parker
|
||||
// This is necessary because isEqualToNumber will return negative integer values that aren't coercable directly to BOOL's without help [sbw]
|
||||
BOOL (*ComparisonSender)(id, SEL, id) = (BOOL (*)(id, SEL, id)) objc_msgSend;
|
||||
BOOL areEqual = ComparisonSender(currentValue, comparisonSelector, propertyValue);
|
||||
|
||||
if (NO == areEqual) {
|
||||
[model setValue:propertyValue forKey:propertyName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setPropertiesOfModel:(NSObject<RKObjectMappable>*)model fromElements:(NSDictionary*)elements {
|
||||
BOOL anyValuesSet = NO;
|
||||
NSDictionary* elementToPropertyMappings = [self elementToPropertyMappingsForModel:model];
|
||||
for (NSString* elementKeyPath in elementToPropertyMappings) {
|
||||
id elementValue = nil;
|
||||
BOOL setValue = YES;
|
||||
|
||||
@try {
|
||||
elementValue = [elements valueForKeyPath:elementKeyPath];
|
||||
}
|
||||
@catch (NSException * e) {
|
||||
NSLog(@"[RestKit] RKModelMapper: Unable to find element at keyPath %@ in elements dictionary for %@. Skipping...", elementKeyPath, [model class]);
|
||||
setValue = NO;
|
||||
}
|
||||
|
||||
// nil is returned when the collection does not contain the element
|
||||
if (nil == elementValue) {
|
||||
setValue = (_missingElementMappingPolicy == RKSetNilForMissingElementMappingPolicy);
|
||||
}
|
||||
|
||||
if (setValue) {
|
||||
anyValuesSet = YES;
|
||||
id propertyValue = elementValue;
|
||||
NSString* propertyName = [elementToPropertyMappings objectForKey:elementKeyPath];
|
||||
Class class = [self typeClassForProperty:propertyName ofClass:[model class]];
|
||||
if (elementValue != (id)kCFNull && nil != elementValue) {
|
||||
if ([class isEqual:[NSDate class]]) {
|
||||
NSDate* date = [self parseDateFromString:(propertyValue)];
|
||||
propertyValue = [self dateInLocalTime:date];
|
||||
}
|
||||
else if ([class isEqual:[NSDecimalNumber class]]) {
|
||||
propertyValue = [NSDecimalNumber decimalNumberWithString:propertyValue];
|
||||
}
|
||||
}
|
||||
|
||||
// If we know the destination class, the property is not of the correct class, and the property value is not null or nil...
|
||||
// if (class && propertyValue && ![propertyValue isKindOfClass:class] && ![propertyValue isEqual:[NSNull null]]) {
|
||||
if (class && propertyValue && ![propertyValue isKindOfClass:class] && ![propertyValue isEqual:[NSNull null]]) {
|
||||
// Then we must cooerce the element (probably a string) into the correct class.
|
||||
// Currently this only supports NSNumbers (NSDates are handled above).
|
||||
// New cooersions will be added on an as-needed basis.
|
||||
if (class == [NSNumber class]) {
|
||||
if ([propertyValue isEqualToString:@"true"] ||
|
||||
[propertyValue isEqualToString:@"false"]) {
|
||||
propertyValue = [NSNumber numberWithBool:[propertyValue isEqualToString:@"true"]];
|
||||
} else {
|
||||
propertyValue = [NSNumber numberWithDouble:[propertyValue doubleValue]];
|
||||
}
|
||||
} else if (class == [NSURL class] && [propertyValue isKindOfClass:[NSString class]]) {
|
||||
propertyValue = [NSURL URLWithString:propertyValue];
|
||||
} else {
|
||||
[NSException raise:@"NoElementValueConversionMethod" format:@"Don't know how to convert %@ (%@) to %@", propertyValue, [propertyValue class], class];
|
||||
}
|
||||
}
|
||||
|
||||
[self updateModel:model ifNewPropertyValue:propertyValue forPropertyNamed:propertyName];
|
||||
}
|
||||
}
|
||||
|
||||
if (anyValuesSet == NO) {
|
||||
NSLog(@"WARNING: RestKit did not find any mappable values for the specified object. If you have specified an explicit objectClass for mapping, be sure that the keyPath has been set to target your mappable data.");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setRelationshipsOfModel:(NSObject<RKObjectMappable>*)object fromElements:(NSDictionary*)elements {
|
||||
NSDictionary* elementToRelationshipMappings = [[object class] elementToRelationshipMappings];
|
||||
for (NSString* elementKeyPath in elementToRelationshipMappings) {
|
||||
NSString* propertyName = [elementToRelationshipMappings objectForKey:elementKeyPath];
|
||||
|
||||
id relationshipElements = nil;
|
||||
@try {
|
||||
relationshipElements = [elements valueForKeyPath:elementKeyPath];
|
||||
}
|
||||
@catch (NSException* e) {
|
||||
NSLog(@"Caught exception:%@ when trying valueForKeyPath with path:%@ for elements:%@", e, elementKeyPath, elements);
|
||||
}
|
||||
|
||||
// Handle missing elements for the relationship
|
||||
if (relationshipElements == nil) {
|
||||
if (self.missingElementMappingPolicy == RKSetNilForMissingElementMappingPolicy) {
|
||||
[object setValue:nil forKey:propertyName];
|
||||
continue;
|
||||
} else if(self.missingElementMappingPolicy == RKIgnoreMissingElementMappingPolicy) {
|
||||
continue;
|
||||
}
|
||||
mappableValue = [self.sourceObject valueForKeyPath:keyPath];
|
||||
}
|
||||
|
||||
// NOTE: The last part of the keyPath contains the elementName for the mapped destination class of our children
|
||||
NSArray* componentsOfKeyPath = [elementKeyPath componentsSeparatedByString:@"."];
|
||||
NSString *className = [componentsOfKeyPath objectAtIndex:[componentsOfKeyPath count] - 1];
|
||||
Class modelClass = [_elementToClassMappings valueForKeyPath:className];
|
||||
if ([modelClass isKindOfClass: [NSNull class]]) {
|
||||
NSLog(@"Warning: could not find a class mapping for relationship '%@':", className);
|
||||
NSLog(@" parent class : %@", [object class]);
|
||||
NSLog(@" elements to map: %@", elements);
|
||||
NSLog(@"maybe you want to register your model with the object mapper or you want to pluralize the keypath?");
|
||||
break;
|
||||
// Not found...
|
||||
if (mappableValue == nil || mappableValue == [NSNull null] || [self isNullCollection:mappableValue]) {
|
||||
RKLOG_MAPPING(RKLogLevelInfo, @"Found unmappable value at keyPath: %@", keyPath);
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(objectMapper:didNotFindMappableObjectAtKeyPath:)]) {
|
||||
[self.delegate objectMapper:self didNotFindMappableObjectAtKeyPath:keyPath];
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Class collectionClass = [self typeClassForProperty:propertyName ofClass:[object class]];
|
||||
if ([collectionClass isSubclassOfClass:[NSSet class]] || [collectionClass isSubclassOfClass:[NSArray class]]) {
|
||||
id children = nil;
|
||||
if ([collectionClass isSubclassOfClass:[NSSet class]]) {
|
||||
children = [NSMutableSet setWithCapacity:[relationshipElements count]];
|
||||
} else if ([collectionClass isSubclassOfClass:[NSArray class]]) {
|
||||
children = [NSMutableArray arrayWithCapacity:[relationshipElements count]];
|
||||
}
|
||||
|
||||
for (NSDictionary* childElements in relationshipElements) {
|
||||
id child = [self createOrUpdateInstanceOfModelClass:modelClass fromElements:childElements];
|
||||
if (child) {
|
||||
[(NSMutableArray*)children addObject:child];
|
||||
}
|
||||
}
|
||||
|
||||
[object setValue:children forKey:propertyName];
|
||||
} else if ([relationshipElements isKindOfClass:[NSDictionary class]]) {
|
||||
id child = [self createOrUpdateInstanceOfModelClass:modelClass fromElements:relationshipElements];
|
||||
[object setValue:child forKey:propertyName];
|
||||
// Found something to map
|
||||
foundMappable = YES;
|
||||
RKObjectMapping* objectMapping = [keyPathsAndObjectMappings objectForKey:keyPath];
|
||||
if ([self.delegate respondsToSelector:@selector(objectMapper:didFindMappableObject:atKeyPath:withMapping:)]) {
|
||||
[self.delegate objectMapper:self didFindMappableObject:mappableValue atKeyPath:keyPath withMapping:objectMapping];
|
||||
}
|
||||
RKLOG_MAPPING(RKLogLevelInfo, @"Found mappable data at keyPath '%@': %@", keyPath, mappableValue);
|
||||
if ([mappableValue isKindOfClass:[NSArray class]] || [mappableValue isKindOfClass:[NSSet class]]) {
|
||||
mappingResult = [self mapCollection:mappableValue atKeyPath:keyPath usingMapping:objectMapping];
|
||||
} else {
|
||||
mappingResult = [self mapObject:mappableValue atKeyPath:keyPath usingMapping:objectMapping];
|
||||
}
|
||||
|
||||
if (mappingResult) {
|
||||
[results setObject:mappingResult forKey:keyPath];
|
||||
}
|
||||
}
|
||||
|
||||
// Connect relationships by primary key
|
||||
Class managedObjectClass = NSClassFromString(@"RKManagedObject");
|
||||
if (managedObjectClass && [object isKindOfClass:managedObjectClass]) {
|
||||
RKManagedObject* managedObject = (RKManagedObject*)object;
|
||||
NSDictionary* relationshipToPkPropertyMappings = [[managedObject class] relationshipToPrimaryKeyPropertyMappings];
|
||||
for (NSString* relationship in relationshipToPkPropertyMappings) {
|
||||
NSString* primaryKeyPropertyString = [relationshipToPkPropertyMappings objectForKey:relationship];
|
||||
|
||||
NSNumber* objectPrimaryKeyValue = nil;
|
||||
@try {
|
||||
objectPrimaryKeyValue = [managedObject valueForKeyPath:primaryKeyPropertyString];
|
||||
} @catch (NSException* e) {
|
||||
NSLog(@"Caught exception:%@ when trying valueForKeyPath with path:%@ for object:%@", e, primaryKeyPropertyString, managedObject);
|
||||
}
|
||||
|
||||
NSDictionary* relationshipsByName = [[managedObject entity] relationshipsByName];
|
||||
NSEntityDescription* relationshipDestinationEntity = [[relationshipsByName objectForKey:relationship] destinationEntity];
|
||||
id relationshipDestinationClass = objc_getClass([[relationshipDestinationEntity managedObjectClassName] cStringUsingEncoding:NSUTF8StringEncoding]);
|
||||
RKManagedObject* relationshipValue = [[[RKObjectManager sharedManager] objectStore] findOrCreateInstanceOfManagedObject:relationshipDestinationClass
|
||||
withPrimaryKeyValue:objectPrimaryKeyValue];
|
||||
if (relationshipValue) {
|
||||
[managedObject setValue:relationshipValue forKey:relationship];
|
||||
}
|
||||
}
|
||||
if ([self.delegate respondsToSelector:@selector(objectMapperDidFinishMapping:)]) {
|
||||
[self.delegate objectMapperDidFinishMapping:self];
|
||||
}
|
||||
|
||||
|
||||
if (foundMappable == NO) {
|
||||
[self addErrorForUnmappableKeyPath:@""];
|
||||
return nil;
|
||||
}
|
||||
|
||||
// If we found a mappable and a mapping but the results remains empty, then an
|
||||
// error occured in the underlying operation and we should return nil to indicate the failure
|
||||
if ([results count] == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
RKLOG_MAPPING(RKLogLevelDebug, @"Finished performing object mapping. Results: %@", results);
|
||||
|
||||
return [RKObjectMappingResult mappingResultWithDictionary:results];
|
||||
}
|
||||
|
||||
- (void)updateModel:(NSObject<RKObjectMappable>*)model fromElements:(NSDictionary*)elements {
|
||||
[self setPropertiesOfModel:model fromElements:elements];
|
||||
[self setRelationshipsOfModel:model fromElements:elements];
|
||||
}
|
||||
#pragma - RKObjectFactory methods
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Date & Time Helpers
|
||||
|
||||
- (NSDate*)parseDateFromString:(NSString*)string {
|
||||
NSDate* date = nil;
|
||||
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
|
||||
// TODO: I changed this to local time and it fixes my date issues. wtf?
|
||||
[formatter setTimeZone:self.localTimeZone];
|
||||
for (NSString* formatString in self.dateFormats) {
|
||||
[formatter setDateFormat:formatString];
|
||||
date = [formatter dateFromString:string];
|
||||
if (date) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[formatter release];
|
||||
return date;
|
||||
}
|
||||
|
||||
- (NSDate*)dateInLocalTime:(NSDate*)date {
|
||||
NSDate* destinationDate = nil;
|
||||
if (date) {
|
||||
NSInteger remoteGMTOffset = [self.remoteTimeZone secondsFromGMTForDate:date];
|
||||
NSInteger localGMTOffset = [self.localTimeZone secondsFromGMTForDate:date];
|
||||
NSTimeInterval interval = localGMTOffset - remoteGMTOffset;
|
||||
destinationDate = [[[NSDate alloc] initWithTimeInterval:interval sinceDate:date] autorelease];
|
||||
}
|
||||
return destinationDate;
|
||||
- (id)objectWithMapping:(RKObjectMapping*)objectMapping andData:(id)mappableData {
|
||||
if (self.objectFactory) {
|
||||
return [self.objectFactory objectWithMapping:objectMapping andData:mappableData];
|
||||
}
|
||||
|
||||
return [[objectMapping.objectClass new] autorelease];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user