// // RKObjectMapper.m // RestKit // // Created by Blake Watters on 5/6/11. // Copyright 2011 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 "RKObjectMapper.h" #import "RKObjectMapperError.h" #import "RKObjectMapper_Private.h" // Set Logging Component #undef RKLogComponent #define RKLogComponent lcl_cRestKitObjectMapping @implementation RKObjectMapper @synthesize sourceObject = _sourceObject; @synthesize targetObject = _targetObject; @synthesize delegate =_delegate; @synthesize mappingProvider = _mappingProvider; @synthesize errors = _errors; + (id)mapperWithObject:(id)object mappingProvider:(RKObjectMappingProvider*)mappingProvider { return [[[self alloc] initWithObject:object mappingProvider:mappingProvider] autorelease]; } - (id)initWithObject:(id)object mappingProvider:(RKObjectMappingProvider*)mappingProvider { self = [super init]; if (self) { _sourceObject = [object retain]; _mappingProvider = mappingProvider; _errors = [NSMutableArray new]; _operationQueue = [RKMappingOperationQueue new]; } return self; } - (void)dealloc { [_sourceObject release]; [_errors release]; [_operationQueue release]; [super dealloc]; } #pragma mark - Errors - (NSUInteger)errorCount { return [self.errors count]; } - (void)addError:(NSError*)error { NSAssert(error, @"Cannot add a nil error"); [_errors addObject:error]; if ([self.delegate respondsToSelector:@selector(objectMapper:didAddError:)]) { [self.delegate objectMapper:self didAddError:error]; } RKLogWarning(@"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. // 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]) { RKLogDebug(@"Found a collection containing only NSNull values, considering the collection unmappable..."); return YES; } } return NO; } #pragma mark - Mapping Primitives - (id)mapObject:(id)mappableObject atKeyPath:(NSString*)keyPath usingMapping:(id)mapping { NSAssert([mappableObject respondsToSelector:@selector(setValue:forKeyPath:)], @"Expected self.object to be KVC compliant"); id destinationObject = nil; if (self.targetObject) { destinationObject = self.targetObject; RKObjectMapping* objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicObjectMapping class]]) { objectMapping = [(RKDynamicObjectMapping*)mapping objectMappingForDictionary:mappableObject]; } else if ([mapping isKindOfClass:[RKObjectMapping class]]) { objectMapping = (RKObjectMapping*)mapping; } else { NSAssert(objectMapping, @"Encountered unknown mapping type '%@'", NSStringFromClass([mapping class])); } if (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:mapping andData:mappableObject]; } if (mapping && destinationObject) { BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; if (success) { return destinationObject; } } else { // Attempted to map an object but couldn't find a mapping for the keyPath [self addErrorForUnmappableKeyPath:keyPath]; return nil; } return nil; } - (NSArray*)mapCollection:(NSArray*)mappableObjects atKeyPath:(NSString*)keyPath usingMapping:(id)mapping { NSAssert(mappableObjects != nil, @"Cannot map without an collection of mappable objects"); NSAssert(mapping != nil, @"Cannot map without a mapping to consult"); NSArray* objectsToMap = mappableObjects; if (mapping.forceCollectionMapping) { // If we have forced mapping of a dictionary, map each subdictionary if ([mappableObjects isKindOfClass:[NSDictionary class]]) { RKLogDebug(@"Collection mapping forced for NSDictionary, mapping each key/value independently..."); objectsToMap = [NSMutableArray arrayWithCapacity:[mappableObjects count]]; for (id key in mappableObjects) { NSDictionary* dictionaryToMap = [NSDictionary dictionaryWithObject:[mappableObjects valueForKey:key] forKey:key]; [(NSMutableArray*)objectsToMap addObject:dictionaryToMap]; } } else { RKLogWarning(@"Collection mapping forced but mappable objects is of type '%@' rather than NSDictionary", NSStringFromClass([mappableObjects class])); } } // 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; } for (id mappableObject in objectsToMap) { id destinationObject = [self objectWithMapping:mapping andData:mappableObject]; if (! destinationObject) { continue; } BOOL success = [self mapFromObject:mappableObject toObject:destinationObject atKeyPath:keyPath usingMapping:mapping]; if (success) { [mappedObjects addObject:destinationObject]; } } return mappedObjects; } // The workhorse of this entire process. Emits object loading operations - (BOOL)mapFromObject:(id)mappableObject toObject:(id)destinationObject atKeyPath:keyPath usingMapping:(id)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"); RKLogDebug(@"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 withMapping:mapping]; operation.queue = _operationQueue; 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 (error) { 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; } - (id)objectWithMapping:(id)mapping andData:(id)mappableData { NSAssert([mapping conformsToProtocol:@protocol(RKObjectMappingDefinition)], @"Expected an object implementing RKObjectMappingDefinition"); RKObjectMapping* objectMapping = nil; if ([mapping isKindOfClass:[RKDynamicObjectMapping class]]) { objectMapping = [(RKDynamicObjectMapping*)mapping objectMappingForDictionary:mappableData]; if (! objectMapping) { RKLogDebug(@"Mapping %@ declined mapping for data %@: returned nil objectMapping", mapping, mappableData); } } else if ([mapping isKindOfClass:[RKObjectMapping class]]) { objectMapping = (RKObjectMapping*)mapping; } else { NSAssert(objectMapping, @"Encountered unknown mapping type '%@'", NSStringFromClass([mapping class])); } if (objectMapping) { return [objectMapping mappableObjectForData:mappableData]; } return nil; } // 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"); RKLogDebug(@"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* mappingsByKeyPath = [self.mappingProvider mappingsByKeyPath]; for (NSString* keyPath in mappingsByKeyPath) { id mappingResult; id mappableValue; RKLogTrace(@"Examining keyPath '%@' for mappable content...", keyPath); if ([keyPath isEqualToString:@""]) { mappableValue = self.sourceObject; } else { mappableValue = [self.sourceObject valueForKeyPath:keyPath]; } // Not found... if (mappableValue == nil || mappableValue == [NSNull null] || [self isNullCollection:mappableValue]) { RKLogDebug(@"Found unmappable value at keyPath: %@", keyPath); if ([self.delegate respondsToSelector:@selector(objectMapper:didNotFindMappableObjectAtKeyPath:)]) { [self.delegate objectMapper:self didNotFindMappableObjectAtKeyPath:keyPath]; } continue; } // Found something to map foundMappable = YES; id mapping = [mappingsByKeyPath objectForKey:keyPath]; if ([self.delegate respondsToSelector:@selector(objectMapper:didFindMappableObject:atKeyPath:withMapping:)]) { [self.delegate objectMapper:self didFindMappableObject:mappableValue atKeyPath:keyPath withMapping:mapping]; } if (mapping.forceCollectionMapping || [mappableValue isKindOfClass:[NSArray class]] || [mappableValue isKindOfClass:[NSSet class]]) { RKLogDebug(@"Found mappable collection at keyPath '%@': %@", keyPath, mappableValue); mappingResult = [self mapCollection:mappableValue atKeyPath:keyPath usingMapping:mapping]; } else { RKLogDebug(@"Found mappable data at keyPath '%@': %@", keyPath, mappableValue); mappingResult = [self mapObject:mappableValue atKeyPath:keyPath usingMapping:mapping]; } if (mappingResult) { [results setObject:mappingResult forKey:keyPath]; } } // Allow any queued operations to complete RKLogDebug(@"The following operations are in the queue: %@", _operationQueue.operations); [_operationQueue waitUntilAllOperationsAreFinished]; if ([self.delegate respondsToSelector:@selector(objectMapperDidFinishMapping:)]) { [self.delegate objectMapperDidFinishMapping:self]; } // If we found nothing eligible for mapping in the content, add an unmappable key path error and fail mapping // If the content is empty, we don't consider it an error BOOL isEmpty = [self.sourceObject respondsToSelector:@selector(count)] && ([self.sourceObject count] == 0); if (foundMappable == NO && !isEmpty) { [self addErrorForUnmappableKeyPath:@""]; return nil; } RKLogDebug(@"Finished performing object mapping. Results: %@", results); return [RKObjectMappingResult mappingResultWithDictionary:results]; } @end