// // RKObjectMappingOperation.m // RestKit // // Created by Blake Watters on 4/30/11. // Copyright 2011 Two Toasters. All rights reserved. // #import #import "RKObjectMappingOperation.h" #import "RKObjectMapperError.h" #import "RKObjectPropertyInspector.h" #import "RKObjectRelationshipMapping.h" #import "RKObjectMapper.h" #import "../Support/Errors.h" #import "../Support/RKLog.h" // Set Logging Component #undef RKLogComponent #define RKLogComponent lcl_cRestKitObjectMapping extern NSString* const RKObjectMappingNestingAttributeKeyName; @implementation RKObjectMappingOperation @synthesize sourceObject = _sourceObject; @synthesize destinationObject = _destinationObject; @synthesize objectMapping = _objectMapping; @synthesize delegate = _delegate; @synthesize objectFactory = _objectFactory; + (RKObjectMappingOperation*)mappingOperationFromObject:(id)sourceObject toObject:(id)destinationObject withObjectMapping:(RKObjectMapping*)objectMapping { return [[[self alloc] initWithSourceObject:sourceObject destinationObject:destinationObject objectMapping:objectMapping] autorelease]; } - (id)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObject objectMapping:(RKObjectMapping*)objectMapping { NSAssert(sourceObject != nil, @"Cannot perform a mapping operation without a sourceObject object"); NSAssert(destinationObject != nil, @"Cannot perform a mapping operation without a destinationObject"); NSAssert(objectMapping != nil, @"Cannot perform a mapping operation without an object mapping to apply"); self = [super init]; if (self) { _sourceObject = [sourceObject retain]; _destinationObject = [destinationObject retain]; _objectMapping = [objectMapping retain]; } return self; } - (void)dealloc { [_sourceObject release]; [_destinationObject release]; [_objectMapping release]; [_nestedAttributeSubstitution release]; [super dealloc]; } - (NSDate*)parseDateFromString:(NSString*)string { RKLogTrace(@"Transforming string value '%@' to NSDate...", string); NSDate* date = nil; NSDateFormatter* formatter = [[NSDateFormatter alloc] init]; formatter.timeZone = [NSTimeZone localTimeZone]; for (NSString* formatString in self.objectMapping.dateFormatStrings) { [formatter setDateFormat:formatString]; date = [formatter dateFromString:string]; if (date) { break; } } [formatter release]; return date; } - (id)transformValue:(id)value atKeyPath:keyPath toType:(Class)destinationType { RKLogTrace(@"Found transformable value at keyPath '%@'. Transforming from type '%@' to '%@'", keyPath, NSStringFromClass([value class]), NSStringFromClass(destinationType)); Class sourceType = [value class]; if ([sourceType isSubclassOfClass:[NSString class]]) { if ([destinationType isSubclassOfClass:[NSDate class]]) { // String -> Date return [self parseDateFromString:(NSString*)value]; } else if ([destinationType isSubclassOfClass:[NSURL class]]) { // String -> URL return [NSURL URLWithString:(NSString*)value]; } else if ([destinationType isSubclassOfClass:[NSDecimalNumber class]]) { // String -> Decimal Number return [NSDecimalNumber decimalNumberWithString:(NSString*)value]; } else if ([destinationType isSubclassOfClass:[NSNumber class]]) { // String -> Number NSString* lowercasedString = [(NSString*)value lowercaseString]; NSSet* trueStrings = [NSSet setWithObjects:@"true", @"t", @"yes", nil]; NSSet* booleanStrings = [trueStrings setByAddingObjectsFromSet:[NSSet setWithObjects:@"false", @"f", @"no", nil]]; if ([booleanStrings containsObject:lowercasedString]) { // Handle booleans encoded as Strings return [NSNumber numberWithBool:[trueStrings containsObject:lowercasedString]]; } else { return [NSNumber numberWithDouble:[(NSString*)value doubleValue]]; } } } else if (value == [NSNull null] || [value isEqual:[NSNull null]]) { // Transform NSNull -> nil for simplicity return nil; } else if ([sourceType isSubclassOfClass:[NSSet class]]) { // Set -> Array if ([destinationType isSubclassOfClass:[NSArray class]]) { return [(NSSet*)value allObjects]; } } else if ([sourceType isSubclassOfClass:[NSArray class]]) { // Array -> Set if ([destinationType isSubclassOfClass:[NSSet class]]) { return [NSSet setWithArray:value]; } } else if ([sourceType isSubclassOfClass:[NSNumber class]]) { // Number -> Date if ([destinationType isSubclassOfClass:[NSDate class]]) { return [NSDate dateWithTimeIntervalSince1970:[(NSNumber*)value intValue]]; } else if ([sourceType isSubclassOfClass:NSClassFromString(@"__NSCFBoolean")] && [destinationType isSubclassOfClass:[NSString class]]) { return ([value boolValue] ? @"true" : @"false"); } } else if ([destinationType isSubclassOfClass:[NSString class]] && [value respondsToSelector:@selector(stringValue)]) { return [value stringValue]; } RKLogWarning(@"Failed transformation of value at keyPath '%@'. No strategy for transforming from '%@' to '%@'", keyPath, NSStringFromClass([value class]), NSStringFromClass(destinationType)); return nil; } - (BOOL)isValue:(id)sourceValue equalToValue:(id)destinationValue { NSAssert(sourceValue, @"Expected sourceValue not to be nil"); NSAssert(destinationValue, @"Expected destinationValue not to be nil"); SEL comparisonSelector; if ([sourceValue isKindOfClass:[NSString class]]) { comparisonSelector = @selector(isEqualToString:); } else if ([sourceValue isKindOfClass:[NSNumber class]]) { comparisonSelector = @selector(isEqualToNumber:); } else if ([sourceValue isKindOfClass:[NSDate class]]) { comparisonSelector = @selector(isEqualToDate:); } else if ([sourceValue isKindOfClass:[NSArray class]]) { comparisonSelector = @selector(isEqualToArray:); } else if ([sourceValue isKindOfClass:[NSDictionary class]]) { comparisonSelector = @selector(isEqualToDictionary:); } else if ([sourceValue isKindOfClass:[NSSet class]]) { comparisonSelector = @selector(isEqualToSet:); } else { comparisonSelector = @selector(isEqual:); } // 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; return ComparisonSender(sourceValue, comparisonSelector, destinationValue); } - (BOOL)validateValue:(id)value atKeyPath:(NSString*)keyPath { BOOL success = YES; if ([self.destinationObject respondsToSelector:@selector(validateValue:forKey:error:)]) { success = [self.destinationObject validateValue:&value forKey:keyPath error:&_validationError]; if (!success) { if (_validationError) { RKLogError(@"Validation failed while mapping attribute at key path %@ to value %@. Error: %@", keyPath, value, [_validationError localizedDescription]); } else { RKLogWarning(@"Destination object %@ rejected attribute value %@ for keyPath %@. Skipping...", self.destinationObject, value, keyPath); } } } return success; } - (BOOL)shouldSetValue:(id)value atKeyPath:(NSString*)keyPath { id currentValue = [self.destinationObject valueForKeyPath:keyPath]; if (currentValue == [NSNull null] || [currentValue isEqual:[NSNull null]]) { currentValue = nil; } if (nil == currentValue && nil == value) { // Both are nil return NO; } else if (nil == value || nil == currentValue) { // One is nil and the other is not return [self validateValue:value atKeyPath:keyPath]; } if (! [self isValue:value equalToValue:currentValue]) { // Validate value for key return [self validateValue:value atKeyPath:keyPath]; } return NO; } - (NSArray*)applyNestingToMappings:(NSArray*)mappings { if (_nestedAttributeSubstitution) { NSString* searchString = [NSString stringWithFormat:@"(%@)", [[_nestedAttributeSubstitution allKeys] lastObject]]; NSString* replacementString = [[_nestedAttributeSubstitution allValues] lastObject]; NSMutableArray* array = [NSMutableArray arrayWithCapacity:[self.objectMapping.attributeMappings count]]; for (RKObjectAttributeMapping* mapping in mappings) { RKObjectAttributeMapping* nestedMapping = [mapping copy]; nestedMapping.sourceKeyPath = [nestedMapping.sourceKeyPath stringByReplacingOccurrencesOfString:searchString withString:replacementString]; nestedMapping.destinationKeyPath = [nestedMapping.destinationKeyPath stringByReplacingOccurrencesOfString:searchString withString:replacementString]; [array addObject:nestedMapping]; [nestedMapping release]; } return array; } return mappings; } - (NSArray*)attributeMappings { return [self applyNestingToMappings:self.objectMapping.attributeMappings]; } - (NSArray*)relationshipMappings { return [self applyNestingToMappings:self.objectMapping.relationshipMappings]; } - (void)applyAttributeMapping:(RKObjectAttributeMapping*)attributeMapping withValue:(id)value { if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didFindMapping:forKeyPath:)]) { [self.delegate objectMappingOperation:self didFindMapping:attributeMapping forKeyPath:attributeMapping.sourceKeyPath]; } RKLogTrace(@"Mapping attribute value keyPath '%@' to '%@'", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath); // Inspect the property type to handle any value transformations Class type = [[RKObjectPropertyInspector sharedInspector] typeForProperty:attributeMapping.destinationKeyPath ofClass:[self.destinationObject class]]; if (type && NO == [[value class] isSubclassOfClass:type]) { value = [self transformValue:value atKeyPath:attributeMapping.sourceKeyPath toType:type]; } // Ensure that the value is different if ([self shouldSetValue:value atKeyPath:attributeMapping.destinationKeyPath]) { RKLogTrace(@"Mapped attribute value from keyPath '%@' to '%@'. Value: %@", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value); [self.destinationObject setValue:value forKey:attributeMapping.destinationKeyPath]; if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didSetValue:forKeyPath:usingMapping:)]) { [self.delegate objectMappingOperation:self didSetValue:value forKeyPath:attributeMapping.destinationKeyPath usingMapping:attributeMapping]; } } else { RKLogTrace(@"Skipped mapping of attribute value from keyPath '%@ to keyPath '%@' -- value is unchanged (%@)", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath, value); } } // Return YES if we mapped any attributes - (BOOL)applyAttributeMappings { // If we have a nesting substitution value, we have alread BOOL appliedMappings = (_nestedAttributeSubstitution != nil); for (RKObjectAttributeMapping* attributeMapping in [self attributeMappings]) { if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) { RKLogTrace(@"Skipping attribute mapping for special keyPath '%@'", RKObjectMappingNestingAttributeKeyName); continue; } id value = nil; if ([attributeMapping.sourceKeyPath isEqualToString:@""]) { value = self.sourceObject; } else { value = [self.sourceObject valueForKeyPath:attributeMapping.sourceKeyPath]; } if (value) { appliedMappings = YES; [self applyAttributeMapping:attributeMapping withValue:value]; } else { if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didNotFindMappingForKeyPath:)]) { [self.delegate objectMappingOperation:self didNotFindMappingForKeyPath:attributeMapping.sourceKeyPath]; } RKLogTrace(@"Did not find mappable attribute value keyPath '%@'", attributeMapping.sourceKeyPath); // Optionally set the default value for missing values if ([self.objectMapping shouldSetDefaultValueForMissingAttributes]) { [self.destinationObject setValue:[self.objectMapping defaultValueForMissingAttribute:attributeMapping.destinationKeyPath] forKey:attributeMapping.destinationKeyPath]; RKLogTrace(@"Setting nil for missing attribute value at keyPath '%@'", attributeMapping.sourceKeyPath); } } // Fail out if an error has occurred if (_validationError) { return NO; } } return appliedMappings; } - (BOOL)isValueACollection:(id)value { return ([value isKindOfClass:[NSSet class]] || [value isKindOfClass:[NSArray class]]); } - (BOOL)mapNestedObject:(id)anObject toObject:(id)anotherObject withMapping:(RKObjectRelationshipMapping*)mapping { NSError* error = nil; RKObjectMappingOperation* subOperation = [RKObjectMappingOperation mappingOperationFromObject:anObject toObject:anotherObject withObjectMapping:mapping.objectMapping]; subOperation.delegate = self.delegate; subOperation.objectFactory = self.objectFactory; if (NO == [subOperation performMapping:&error]) { RKLogWarning(@"WARNING: Failed mapping nested object: %@", [error localizedDescription]); } return YES; } - (BOOL)applyRelationshipMappings { BOOL appliedMappings = NO; id destinationObject = nil; for (RKObjectRelationshipMapping* mapping in [self relationshipMappings]) { id value = [self.sourceObject valueForKeyPath:mapping.sourceKeyPath]; if (value == nil || value == [NSNull null] || [value isEqual:[NSNull null]]) { RKLogDebug(@"Did not find mappable relationship value keyPath '%@'", mapping.sourceKeyPath); // Optionally nil out the property if ([self.objectMapping setNilForMissingRelationships] && [self shouldSetValue:nil atKeyPath:mapping.destinationKeyPath]) { RKLogTrace(@"Setting nil for missing relationship value at keyPath '%@'", mapping.sourceKeyPath); [self.destinationObject setValue:nil forKey:mapping.destinationKeyPath]; } continue; } if ([self isValueACollection:value]) { // One to many relationship RKLogDebug(@"Mapping one to many relationship value at keyPath '%@' to '%@'", mapping.sourceKeyPath, mapping.destinationKeyPath); appliedMappings = YES; destinationObject = [NSMutableArray arrayWithCapacity:[value count]]; for (id nestedObject in value) { id mappedObject = [self.objectFactory objectWithMapping:mapping.objectMapping andData:nestedObject]; if ([self mapNestedObject:nestedObject toObject:mappedObject withMapping:mapping]) { [destinationObject addObject:mappedObject]; } } // Transform from NSSet <-> NSArray if necessary Class type = [[RKObjectPropertyInspector sharedInspector] typeForProperty:mapping.destinationKeyPath ofClass:[self.destinationObject class]]; if (type && NO == [[destinationObject class] isSubclassOfClass:type]) { destinationObject = [self transformValue:destinationObject atKeyPath:mapping.sourceKeyPath toType:type]; } } else { // One to one relationship RKLogDebug(@"Mapping one to one relationship value at keyPath '%@' to '%@'", mapping.sourceKeyPath, mapping.destinationKeyPath); destinationObject = [self.objectFactory objectWithMapping:mapping.objectMapping andData:value]; NSAssert(destinationObject, @"Cannot map a relationship without an object factory to create it..."); if ([self mapNestedObject:value toObject:destinationObject withMapping:mapping]) { appliedMappings = YES; } } // If the relationship has changed, set it if ([self shouldSetValue:destinationObject atKeyPath:mapping.destinationKeyPath]) { RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", mapping.sourceKeyPath, mapping.destinationKeyPath, destinationObject); [self.destinationObject setValue:destinationObject forKey:mapping.destinationKeyPath]; } // Fail out if a validation error has occurred if (_validationError) { return NO; } } return appliedMappings; } - (void)applyNestedMappings { RKObjectAttributeMapping* attributeMapping = [self.objectMapping mappingForKeyPath:RKObjectMappingNestingAttributeKeyName]; if (attributeMapping) { RKLogDebug(@"Found nested mapping definition to attribute '%@'", attributeMapping.destinationKeyPath); id attributeValue = [[self.sourceObject allKeys] lastObject]; if (attributeValue) { RKLogDebug(@"Found nesting value of '%@' for attribute '%@'", attributeValue, attributeMapping.destinationKeyPath); _nestedAttributeSubstitution = [[NSDictionary alloc] initWithObjectsAndKeys:attributeValue, attributeMapping.destinationKeyPath, nil]; [self applyAttributeMapping:attributeMapping withValue:attributeValue]; } else { RKLogWarning(@"Unable to find nesting value for attribute '%@'", attributeMapping.destinationKeyPath); } } } - (BOOL)performMapping:(NSError**)error { RKLogDebug(@"Starting mapping operation..."); [self applyNestedMappings]; BOOL mappedAttributes = [self applyAttributeMappings]; BOOL mappedRelationships = [self applyRelationshipMappings]; if ((mappedAttributes || mappedRelationships) && _validationError == nil) { RKLogDebug(@"Finished mapping operation successfully..."); return YES; } // We have failed, see if its because of validation or no mappable content found NSError* failureError = _validationError; if (!failureError) { // We have failed, but not due to validation. So no mappable content was found, construct an error NSDictionary* userInfo = [NSDictionary dictionaryWithObjectsAndKeys: @"No mappable attributes or relationships found.", NSLocalizedDescriptionKey, nil]; failureError = [NSError errorWithDomain:RKRestKitErrorDomain code:RKObjectMapperErrorUnmappableContent userInfo:userInfo]; } if (error) *error = failureError; if ([self.delegate respondsToSelector:@selector(objectMappingOperation:didFailWithError:)]) { [self.delegate objectMappingOperation:self didFailWithError:failureError]; } RKLogError(@"Failed mapping operation: %@", [failureError localizedDescription]); return NO; } - (NSString*)description { return [NSString stringWithFormat:@"RKObjectMappingOperation for '%@' object. Mapping values from object %@ to object %@ with object mapping %@", NSStringFromClass([self.destinationObject class]), self.sourceObject, self.destinationObject, self.objectMapping]; } @end