mirror of
https://github.com/zhigang1992/RestKit.git
synced 2026-01-12 22:51:50 +08:00
823 lines
41 KiB
Objective-C
823 lines
41 KiB
Objective-C
//
|
|
// RKMappingOperation.m
|
|
// RestKit
|
|
//
|
|
// Created by Blake Watters on 4/30/11.
|
|
// Copyright (c) 2009-2012 RestKit. All rights reserved.
|
|
//
|
|
// 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 "RKMappingOperation.h"
|
|
#import "RKMappingErrors.h"
|
|
#import "RKPropertyInspector.h"
|
|
#import "RKAttributeMapping.h"
|
|
#import "RKRelationshipMapping.h"
|
|
#import "RKErrors.h"
|
|
#import "RKLog.h"
|
|
#import "RKMappingOperationDataSource.h"
|
|
#import "RKObjectMappingOperationDataSource.h"
|
|
#import "RKDynamicMapping.h"
|
|
#import "RKObjectUtilities.h"
|
|
#import "RKValueTransformers.h"
|
|
|
|
// Set Logging Component
|
|
#undef RKLogComponent
|
|
#define RKLogComponent RKlcl_cRestKitObjectMapping
|
|
|
|
extern NSString * const RKObjectMappingNestingAttributeKeyName;
|
|
|
|
// Defined in RKObjectMapping.h
|
|
NSDate *RKDateFromStringWithFormatters(NSString *dateString, NSArray *formatters);
|
|
|
|
/**
|
|
This function ensures that attribute mappings apply cleanly to an `NSMutableDictionary` target class to support mapping to nested keyPaths. See issue #882
|
|
*/
|
|
static void RKSetIntermediateDictionaryValuesOnObjectForKeyPath(id object, NSString *keyPath)
|
|
{
|
|
if (! [object isKindOfClass:[NSMutableDictionary class]]) return;
|
|
NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."];
|
|
if ([keyPathComponents count] > 1) {
|
|
for (NSUInteger index = 0; index < [keyPathComponents count] - 1; index++) {
|
|
NSString *intermediateKeyPath = [[keyPathComponents subarrayWithRange:NSMakeRange(0, index + 1)] componentsJoinedByString:@"."];
|
|
if (! [object valueForKeyPath:intermediateKeyPath]) {
|
|
[object setValue:[NSMutableDictionary dictionary] forKeyPath:intermediateKeyPath];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static BOOL RKIsManagedObject(id object)
|
|
{
|
|
Class managedObjectClass = NSClassFromString(@"NSManagedObject");
|
|
return managedObjectClass && [object isKindOfClass:managedObjectClass];
|
|
}
|
|
|
|
id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransformer *dateToStringValueTransformer);
|
|
id RKTransformedValueWithClass(id value, Class destinationType, NSValueTransformer *dateToStringValueTransformer)
|
|
{
|
|
Class sourceType = [value class];
|
|
|
|
if ([value isKindOfClass:destinationType]) {
|
|
// No transformation necessary
|
|
return value;
|
|
} else if ([destinationType isSubclassOfClass:[NSDictionary class]]) {
|
|
return [NSMutableDictionary dictionaryWithObject:[NSMutableDictionary dictionary] forKey:value];
|
|
} else if (RKClassIsCollection(destinationType) && !RKObjectIsCollection(value)) {
|
|
// Call ourself recursively with an array value to transform as appropriate
|
|
return RKTransformedValueWithClass(@[ value ], destinationType, dateToStringValueTransformer);
|
|
} else if ([sourceType isSubclassOfClass:[NSString class]] && [destinationType isSubclassOfClass:[NSDate class]]) {
|
|
// String -> Date
|
|
return [dateToStringValueTransformer transformedValue:value];
|
|
} else if ([destinationType isSubclassOfClass:[NSString class]] && [value isKindOfClass:[NSDate class]]) {
|
|
// NSDate -> NSString
|
|
// Transform using the preferred date formatter
|
|
return [dateToStringValueTransformer reverseTransformedValue:value];
|
|
} else if ([destinationType isSubclassOfClass:[NSData class]]) {
|
|
return [NSKeyedArchiver archivedDataWithRootObject:value];
|
|
} else if ([sourceType isSubclassOfClass:[NSString class]]) {
|
|
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 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:[NSOrderedSet class]]) {
|
|
// OrderedSet -> Array
|
|
if ([destinationType isSubclassOfClass:[NSArray class]]) {
|
|
return [value array];
|
|
}
|
|
} else if ([sourceType isSubclassOfClass:[NSArray class]]) {
|
|
// Array -> Set
|
|
if ([destinationType isSubclassOfClass:[NSSet class]]) {
|
|
return [NSSet setWithArray:value];
|
|
}
|
|
// Array -> OrderedSet
|
|
if ([destinationType isSubclassOfClass:[NSOrderedSet class]]) {
|
|
return [[NSOrderedSet class] orderedSetWithArray:value];
|
|
}
|
|
} else if ([sourceType isSubclassOfClass:[NSNumber class]] && [destinationType isSubclassOfClass:[NSDate 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");
|
|
}
|
|
return [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value doubleValue]];
|
|
} else if ([sourceType isSubclassOfClass:[NSNumber class]] && [destinationType isSubclassOfClass:[NSDecimalNumber class]]) {
|
|
// Number -> Decimal Number
|
|
return [NSDecimalNumber decimalNumberWithDecimal:[value decimalValue]];
|
|
} else if ( ([sourceType isSubclassOfClass:NSClassFromString(@"__NSCFBoolean")] ||
|
|
[sourceType isSubclassOfClass:NSClassFromString(@"NSCFBoolean")] ) &&
|
|
[destinationType isSubclassOfClass:[NSString class]]) {
|
|
return ([value boolValue] ? @"true" : @"false");
|
|
if ([destinationType isSubclassOfClass:[NSDate class]]) {
|
|
return [NSDate dateWithTimeIntervalSince1970:[(NSNumber *)value intValue]];
|
|
} else if (([sourceType isSubclassOfClass:NSClassFromString(@"__NSCFBoolean")] || [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];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
// Returns the appropriate value for `nil` value of a primitive type
|
|
static id RKPrimitiveValueForNilValueOfClass(Class keyValueCodingClass)
|
|
{
|
|
if ([keyValueCodingClass isSubclassOfClass:[NSNumber class]]) {
|
|
return @(0);
|
|
} else {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
// Key comes from: [[_nestedAttributeSubstitution allKeys] lastObject]] AND [[_nestedAttributeSubstitution allValues] lastObject];
|
|
NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id value, NSArray *propertyMappings);
|
|
NSArray *RKApplyNestingAttributeValueToMappings(NSString *attributeName, id value, NSArray *propertyMappings)
|
|
{
|
|
if (!attributeName) return propertyMappings;
|
|
|
|
NSString *searchString = [NSString stringWithFormat:@"(%@)", attributeName];
|
|
NSString *replacementString = [NSString stringWithFormat:@"%@", value];
|
|
NSMutableArray *nestedMappings = [NSMutableArray arrayWithCapacity:[propertyMappings count]];
|
|
for (RKPropertyMapping *propertyMapping in propertyMappings) {
|
|
NSString *sourceKeyPath = [propertyMapping.sourceKeyPath stringByReplacingOccurrencesOfString:searchString withString:replacementString];
|
|
NSString *destinationKeyPath = [propertyMapping.destinationKeyPath stringByReplacingOccurrencesOfString:searchString withString:replacementString];
|
|
RKPropertyMapping *nestedMapping = nil;
|
|
if ([propertyMapping isKindOfClass:[RKAttributeMapping class]]) {
|
|
nestedMapping = [RKAttributeMapping attributeMappingFromKeyPath:sourceKeyPath toKeyPath:destinationKeyPath];
|
|
} else if ([propertyMapping isKindOfClass:[RKRelationshipMapping class]]) {
|
|
nestedMapping = [RKRelationshipMapping relationshipMappingFromKeyPath:sourceKeyPath
|
|
toKeyPath:destinationKeyPath
|
|
withMapping:[(RKRelationshipMapping *)propertyMapping mapping]];
|
|
}
|
|
[nestedMappings addObject:nestedMapping];
|
|
}
|
|
|
|
return nestedMappings;
|
|
}
|
|
|
|
static void RKSetValueForObject(id value, id destinationObject)
|
|
{
|
|
if ([destinationObject isKindOfClass:[NSMutableDictionary class]] && [value isKindOfClass:[NSDictionary class]]) {
|
|
[destinationObject setDictionary:value];
|
|
} else {
|
|
[NSException raise:NSInvalidArgumentException format:@"Unable to set value for destination object of type '%@': no strategy available. %@ to %@", [destinationObject class], value, destinationObject];
|
|
}
|
|
}
|
|
|
|
// Returns YES if there is a value present for at least one key path in the given collection
|
|
static BOOL RKObjectContainsValueForKeyPaths(id representation, NSArray *keyPaths)
|
|
{
|
|
for (NSString *keyPath in keyPaths) {
|
|
if ([representation valueForKeyPath:keyPath]) return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
@interface RKMappingOperation ()
|
|
@property (nonatomic, strong, readwrite) RKMapping *mapping;
|
|
@property (nonatomic, strong, readwrite) id sourceObject;
|
|
@property (nonatomic, strong, readwrite) id destinationObject;
|
|
@property (nonatomic, strong) NSDictionary *nestedAttributeSubstitution;
|
|
@property (nonatomic, strong, readwrite) NSError *error;
|
|
@property (nonatomic, strong, readwrite) RKObjectMapping *objectMapping; // The concrete mapping
|
|
@end
|
|
|
|
@implementation RKMappingOperation
|
|
|
|
- (id)initWithSourceObject:(id)sourceObject destinationObject:(id)destinationObject mapping:(RKMapping *)objectOrDynamicMapping
|
|
{
|
|
NSAssert(sourceObject != nil, @"Cannot perform a mapping operation without a sourceObject object");
|
|
NSAssert(objectOrDynamicMapping != nil, @"Cannot perform a mapping operation without a mapping");
|
|
|
|
self = [super init];
|
|
if (self) {
|
|
self.sourceObject = sourceObject;
|
|
self.destinationObject = destinationObject;
|
|
self.mapping = objectOrDynamicMapping;
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (id)destinationObjectForMappingRepresentation:(id)representation withMapping:(RKMapping *)mapping
|
|
{
|
|
RKObjectMapping *concreteMapping = nil;
|
|
if ([mapping isKindOfClass:[RKDynamicMapping class]]) {
|
|
concreteMapping = [(RKDynamicMapping *)mapping objectMappingForRepresentation:representation];
|
|
if (! concreteMapping) {
|
|
RKLogDebug(@"Unable to determine concrete object mapping from dynamic mapping %@ with which to map object representation: %@", mapping, representation);
|
|
return nil;
|
|
}
|
|
} else if ([mapping isKindOfClass:[RKObjectMapping class]]) {
|
|
concreteMapping = (RKObjectMapping *)mapping;
|
|
}
|
|
|
|
return [self.dataSource mappingOperation:self targetObjectForRepresentation:representation withMapping:concreteMapping];
|
|
}
|
|
|
|
- (NSDate *)parseDateFromString:(NSString *)string
|
|
{
|
|
RKLogTrace(@"Transforming string value '%@' to NSDate...", string);
|
|
return RKDateFromStringWithFormatters(string, self.objectMapping.dateFormatters);
|
|
}
|
|
|
|
- (id)transformValue:(id)value atKeyPath:(NSString *)keyPath toType:(Class)destinationType
|
|
{
|
|
RKLogTrace(@"Found transformable value at keyPath '%@'. Transforming from type '%@' to '%@'", keyPath, NSStringFromClass([value class]), NSStringFromClass(destinationType));
|
|
RKDateToStringValueTransformer *transformer = [[RKDateToStringValueTransformer alloc] initWithDateToStringFormatter:self.objectMapping.preferredDateFormatter stringToDateFormatters:self.objectMapping.dateFormatters];
|
|
id transformedValue = RKTransformedValueWithClass(value, destinationType, transformer);
|
|
if (transformedValue != value) return transformedValue;
|
|
|
|
RKLogWarning(@"Failed transformation of value at keyPath '%@'. No strategy for transforming from '%@' to '%@'", keyPath, NSStringFromClass([value class]), NSStringFromClass(destinationType));
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (BOOL)validateValue:(id *)value atKeyPath:(NSString *)keyPath
|
|
{
|
|
BOOL success = YES;
|
|
|
|
if (self.objectMapping.performKeyValueValidation && [self.destinationObject respondsToSelector:@selector(validateValue:forKeyPath:error:)]) {
|
|
NSError *validationError;
|
|
success = [self.destinationObject validateValue:value forKeyPath:keyPath error:&validationError];
|
|
if (!success) {
|
|
self.error = validationError;
|
|
if (validationError) {
|
|
RKLogError(@"Validation failed while mapping attribute at key path '%@' to value %@. Error: %@", keyPath, *value, [validationError localizedDescription]);
|
|
RKLogValidationError(validationError);
|
|
} 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 = nil;
|
|
}
|
|
|
|
/*
|
|
WTF - This workaround should not be necessary, but I have been unable to replicate
|
|
the circumstances that trigger it in a unit test to fix elsewhere. The proper place
|
|
to handle it is in transformValue:atKeyPath:toType:
|
|
|
|
See issue & pull request: https://github.com/RestKit/RestKit/pull/436
|
|
*/
|
|
if (*value == [NSNull null]) {
|
|
RKLogWarning(@"Coercing NSNull value to nil in shouldSetValue:atKeyPath: -- should be fixed.");
|
|
*value = 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 (! RKObjectIsEqualToObject(*value, currentValue)) {
|
|
// Validate value for key
|
|
return [self validateValue:value atKeyPath:keyPath];
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
- (NSArray *)applyNestingToMappings:(NSArray *)mappings
|
|
{
|
|
if (_nestedAttributeSubstitution) {
|
|
return RKApplyNestingAttributeValueToMappings([[_nestedAttributeSubstitution allKeys] lastObject], [[_nestedAttributeSubstitution allValues] lastObject], mappings);
|
|
}
|
|
|
|
return mappings;
|
|
}
|
|
|
|
- (NSArray *)simpleAttributeMappings
|
|
{
|
|
NSMutableArray *mappings = [NSMutableArray array];
|
|
for (RKAttributeMapping *mapping in [self applyNestingToMappings:self.objectMapping.attributeMappings]) {
|
|
if ([mapping.sourceKeyPath rangeOfString:@"."].location == NSNotFound) {
|
|
[mappings addObject:mapping];
|
|
}
|
|
}
|
|
|
|
return mappings;
|
|
}
|
|
|
|
- (NSArray *)keyPathAttributeMappings
|
|
{
|
|
NSMutableArray *mappings = [NSMutableArray array];
|
|
for (RKAttributeMapping *mapping in [self applyNestingToMappings:self.objectMapping.attributeMappings]) {
|
|
if ([mapping.sourceKeyPath rangeOfString:@"."].location != NSNotFound) {
|
|
[mappings addObject:mapping];
|
|
}
|
|
}
|
|
|
|
return mappings;
|
|
}
|
|
|
|
- (NSArray *)relationshipMappings
|
|
{
|
|
return [self applyNestingToMappings:self.objectMapping.relationshipMappings];
|
|
}
|
|
|
|
- (void)applyAttributeMapping:(RKAttributeMapping *)attributeMapping withValue:(id)value
|
|
{
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didFindValue:forKeyPath:mapping:)]) {
|
|
[self.delegate mappingOperation:self didFindValue:value forKeyPath:attributeMapping.sourceKeyPath mapping:attributeMapping];
|
|
}
|
|
RKLogTrace(@"Mapping attribute value keyPath '%@' to '%@'", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath);
|
|
|
|
// Inspect the property type to handle any value transformations
|
|
Class type = [self.objectMapping classForKeyPath:attributeMapping.destinationKeyPath];
|
|
if (type && NO == [[value class] isSubclassOfClass:type]) {
|
|
value = [self transformValue:value atKeyPath:attributeMapping.sourceKeyPath toType:type];
|
|
}
|
|
|
|
// If we have a nil value for a primitive property, we need to coerce it into a KVC usable value or bail out
|
|
if (value == nil && RKPropertyInspectorIsPropertyAtKeyPathOfObjectPrimitive(attributeMapping.destinationKeyPath, self.destinationObject)) {
|
|
RKLogDebug(@"Detected `nil` value transformation for primitive property at keyPath '%@'", attributeMapping.destinationKeyPath);
|
|
value = RKPrimitiveValueForNilValueOfClass(type);
|
|
if (! value) {
|
|
RKLogTrace(@"Skipped mapping of attribute value from keyPath '%@ to keyPath '%@' -- Unable to transform `nil` into primitive value representation", attributeMapping.sourceKeyPath, attributeMapping.destinationKeyPath);
|
|
return;
|
|
}
|
|
}
|
|
|
|
RKSetIntermediateDictionaryValuesOnObjectForKeyPath(self.destinationObject, attributeMapping.destinationKeyPath);
|
|
|
|
// 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);
|
|
|
|
if (attributeMapping.destinationKeyPath) {
|
|
[self.destinationObject setValue:value forKeyPath:attributeMapping.destinationKeyPath];
|
|
} else {
|
|
RKSetValueForObject(value, self.destinationObject);
|
|
}
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didSetValue:forKeyPath:usingMapping:)]) {
|
|
[self.delegate mappingOperation: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);
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didNotSetUnchangedValue:forKeyPath:usingMapping:)]) {
|
|
[self.delegate mappingOperation:self didNotSetUnchangedValue:value forKeyPath:attributeMapping.destinationKeyPath usingMapping:attributeMapping];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return YES if we mapped any attributes
|
|
- (BOOL)applyAttributeMappings:(NSArray *)attributeMappings
|
|
{
|
|
// If we have a nesting substitution value, we have already succeeded
|
|
BOOL appliedMappings = (_nestedAttributeSubstitution != nil);
|
|
|
|
if (!self.objectMapping.performKeyValueValidation) {
|
|
RKLogDebug(@"Key-value validation is disabled for mapping, skipping...");
|
|
}
|
|
|
|
for (RKAttributeMapping *attributeMapping in attributeMappings) {
|
|
if ([self isCancelled]) return NO;
|
|
|
|
if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) {
|
|
RKLogTrace(@"Skipping attribute mapping for special keyPath '%@'", attributeMapping.sourceKeyPath);
|
|
continue;
|
|
}
|
|
|
|
id value = (attributeMapping.sourceKeyPath == nil) ? self.sourceObject : [self.sourceObject valueForKeyPath:attributeMapping.sourceKeyPath];
|
|
if (value) {
|
|
appliedMappings = YES;
|
|
[self applyAttributeMapping:attributeMapping withValue:value];
|
|
} else {
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didNotFindValueForKeyPath:mapping:)]) {
|
|
[self.delegate mappingOperation:self didNotFindValueForKeyPath:attributeMapping.sourceKeyPath mapping:attributeMapping];
|
|
}
|
|
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 defaultValueForAttribute:attributeMapping.destinationKeyPath]
|
|
forKeyPath:attributeMapping.destinationKeyPath];
|
|
RKLogTrace(@"Setting nil for missing attribute value at keyPath '%@'", attributeMapping.sourceKeyPath);
|
|
}
|
|
}
|
|
|
|
// Fail out if an error has occurred
|
|
if (self.error) break;
|
|
}
|
|
|
|
return appliedMappings;
|
|
}
|
|
|
|
- (BOOL)mapNestedObject:(id)anObject toObject:(id)anotherObject withRelationshipMapping:(RKRelationshipMapping *)relationshipMapping
|
|
{
|
|
NSAssert(anObject, @"Cannot map nested object without a nested source object");
|
|
NSAssert(anotherObject, @"Cannot map nested object without a destination object");
|
|
NSAssert(relationshipMapping, @"Cannot map a nested object relationship without a relationship mapping");
|
|
NSError *error = nil;
|
|
|
|
RKLogTrace(@"Performing nested object mapping using mapping %@ for data: %@", relationshipMapping, anObject);
|
|
RKMappingOperation *subOperation = [[RKMappingOperation alloc] initWithSourceObject:anObject destinationObject:anotherObject mapping:relationshipMapping.mapping];
|
|
subOperation.dataSource = self.dataSource;
|
|
subOperation.delegate = self.delegate;
|
|
[subOperation start];
|
|
|
|
if (subOperation.error) {
|
|
RKLogWarning(@"WARNING: Failed mapping nested object: %@", [error localizedDescription]);
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)applyReplaceAssignmentPolicyForRelationshipMapping:(RKRelationshipMapping *)relationshipMapping
|
|
{
|
|
if (relationshipMapping.assignmentPolicy == RKReplaceAssignmentPolicy) {
|
|
if ([self.dataSource respondsToSelector:@selector(mappingOperation:deleteExistingValueOfRelationshipWithMapping:error:)]) {
|
|
NSError *error = nil;
|
|
BOOL success = [self.dataSource mappingOperation:self deleteExistingValueOfRelationshipWithMapping:relationshipMapping error:&error];
|
|
if (! success) {
|
|
RKLogError(@"Failed to delete existing value of relationship mapped with RKReplaceAssignmentPolicy: %@", error);
|
|
self.error = error;
|
|
return NO;
|
|
}
|
|
} else {
|
|
RKLogWarning(@"Requested mapping with `RKReplaceAssignmentPolicy` assignment policy, but the data source does not support it. Mapping has proceeded identically to the `RKSetAssignmentPolicy`.");
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)mapOneToOneRelationshipWithValue:(id)value mapping:(RKRelationshipMapping *)relationshipMapping
|
|
{
|
|
// One to one relationship
|
|
RKLogDebug(@"Mapping one to one relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath);
|
|
|
|
if (relationshipMapping.assignmentPolicy == RKUnionAssignmentPolicy) {
|
|
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Invalid assignment policy: cannot union a one-to-one relationship." };
|
|
self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorInvalidAssignmentPolicy userInfo:userInfo];
|
|
return NO;
|
|
}
|
|
|
|
id destinationObject = [self destinationObjectForMappingRepresentation:value withMapping:relationshipMapping.mapping];
|
|
if (! destinationObject) {
|
|
RKLogDebug(@"Mapping %@ declined mapping for representation %@: returned `nil` destination object.", relationshipMapping.mapping, destinationObject);
|
|
return NO;
|
|
}
|
|
[self mapNestedObject:value toObject:destinationObject withRelationshipMapping:relationshipMapping];
|
|
|
|
// If the relationship has changed, set it
|
|
if ([self shouldSetValue:&destinationObject atKeyPath:relationshipMapping.destinationKeyPath]) {
|
|
if (! [self applyReplaceAssignmentPolicyForRelationshipMapping:relationshipMapping]) {
|
|
return NO;
|
|
}
|
|
|
|
RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, destinationObject);
|
|
[self.destinationObject setValue:destinationObject forKey:relationshipMapping.destinationKeyPath];
|
|
} else {
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didNotSetUnchangedValue:forKeyPath:usingMapping:)]) {
|
|
[self.delegate mappingOperation:self didNotSetUnchangedValue:destinationObject forKeyPath:relationshipMapping.destinationKeyPath usingMapping:relationshipMapping];
|
|
}
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)mapCoreDataToManyRelationshipValue:(id)valueForRelationship withMapping:(RKRelationshipMapping *)relationshipMapping
|
|
{
|
|
if (! RKIsManagedObject(self.destinationObject)) return NO;
|
|
|
|
RKLogTrace(@"Mapping a to-many relationship for an `NSManagedObject`. About to apply value via mutable[Set|Array]ValueForKey");
|
|
if ([valueForRelationship isKindOfClass:[NSSet class]]) {
|
|
RKLogTrace(@"Mapped `NSSet` relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, valueForRelationship);
|
|
NSMutableSet *destinationSet = [self.destinationObject mutableSetValueForKeyPath:relationshipMapping.destinationKeyPath];
|
|
[destinationSet setSet:valueForRelationship];
|
|
} else if ([valueForRelationship isKindOfClass:[NSArray class]]) {
|
|
RKLogTrace(@"Mapped `NSArray` relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, valueForRelationship);
|
|
NSMutableArray *destinationArray = [self.destinationObject mutableArrayValueForKeyPath:relationshipMapping.destinationKeyPath];
|
|
[destinationArray setArray:valueForRelationship];
|
|
} else if ([valueForRelationship isKindOfClass:[NSOrderedSet class]]) {
|
|
RKLogTrace(@"Mapped `NSOrderedSet` relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, valueForRelationship);
|
|
[self.destinationObject setValue:valueForRelationship forKeyPath:relationshipMapping.destinationKeyPath];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)mapOneToManyRelationshipWithValue:(id)value mapping:(RKRelationshipMapping *)relationshipMapping
|
|
{
|
|
// One to many relationship
|
|
RKLogDebug(@"Mapping one to many relationship value at keyPath '%@' to '%@'", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath);
|
|
|
|
NSMutableArray *relationshipCollection = [NSMutableArray arrayWithCapacity:[value count]];
|
|
if (RKObjectIsCollectionOfCollections(value)) {
|
|
RKLogWarning(@"WARNING: Detected a relationship mapping for a collection containing another collection. This is probably not what you want. Consider using a KVC collection operator (such as @unionOfArrays) to flatten your mappable collection.");
|
|
RKLogWarning(@"Key path '%@' yielded collection containing another collection rather than a collection of objects: %@", relationshipMapping.sourceKeyPath, value);
|
|
}
|
|
|
|
if (relationshipMapping.assignmentPolicy == RKUnionAssignmentPolicy) {
|
|
RKLogDebug(@"Mapping relationship with union assignment policy: constructing combined relationship value.");
|
|
id existingObjects = [self.destinationObject valueForKeyPath:relationshipMapping.destinationKeyPath];
|
|
NSArray *existingObjectsArray = RKTransformedValueWithClass(existingObjects, [NSArray class], nil);
|
|
[relationshipCollection addObjectsFromArray:existingObjectsArray];
|
|
}
|
|
|
|
for (id nestedObject in value) {
|
|
id mappableObject = [self destinationObjectForMappingRepresentation:nestedObject withMapping:relationshipMapping.mapping];
|
|
if (! mappableObject) {
|
|
RKLogDebug(@"Mapping %@ declined mapping for representation %@: returned `nil` destination object.", relationshipMapping.mapping, nestedObject);
|
|
continue;
|
|
}
|
|
if ([self mapNestedObject:nestedObject toObject:mappableObject withRelationshipMapping:relationshipMapping]) {
|
|
[relationshipCollection addObject:mappableObject];
|
|
}
|
|
}
|
|
|
|
id valueForRelationship = relationshipCollection;
|
|
// Transform from NSSet <-> NSArray if necessary
|
|
Class type = [self.objectMapping classForKeyPath:relationshipMapping.destinationKeyPath];
|
|
if (type && NO == [[relationshipCollection class] isSubclassOfClass:type]) {
|
|
valueForRelationship = [self transformValue:relationshipCollection atKeyPath:relationshipMapping.sourceKeyPath toType:type];
|
|
}
|
|
|
|
// If the relationship has changed, set it
|
|
if ([self shouldSetValue:&valueForRelationship atKeyPath:relationshipMapping.destinationKeyPath]) {
|
|
if (! [self applyReplaceAssignmentPolicyForRelationshipMapping:relationshipMapping]) {
|
|
return NO;
|
|
}
|
|
if (! [self mapCoreDataToManyRelationshipValue:valueForRelationship withMapping:relationshipMapping]) {
|
|
RKLogTrace(@"Mapped relationship object from keyPath '%@' to '%@'. Value: %@", relationshipMapping.sourceKeyPath, relationshipMapping.destinationKeyPath, valueForRelationship);
|
|
[self.destinationObject setValue:valueForRelationship forKeyPath:relationshipMapping.destinationKeyPath];
|
|
}
|
|
} else {
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didNotSetUnchangedValue:forKeyPath:usingMapping:)]) {
|
|
[self.delegate mappingOperation:self didNotSetUnchangedValue:valueForRelationship forKeyPath:relationshipMapping.destinationKeyPath usingMapping:relationshipMapping];
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)applyRelationshipMappings
|
|
{
|
|
NSAssert(self.dataSource, @"Cannot perform relationship mapping without a data source");
|
|
NSMutableArray *mappingsApplied = [NSMutableArray array];
|
|
|
|
for (RKRelationshipMapping *relationshipMapping in [self relationshipMappings]) {
|
|
if ([self isCancelled]) return NO;
|
|
|
|
id value = nil;
|
|
if (relationshipMapping.sourceKeyPath) {
|
|
value = [self.sourceObject valueForKeyPath:relationshipMapping.sourceKeyPath];
|
|
} else {
|
|
// The nil source keyPath indicates that we want to map directly from the parent representation
|
|
value = self.sourceObject;
|
|
RKObjectMapping *objectMapping = nil;
|
|
|
|
if ([relationshipMapping.mapping isKindOfClass:[RKObjectMapping class]]) {
|
|
objectMapping = (RKObjectMapping *)relationshipMapping.mapping;
|
|
} else if ([relationshipMapping.mapping isKindOfClass:[RKDynamicMapping class]]) {
|
|
objectMapping = [(RKDynamicMapping *)relationshipMapping.mapping objectMappingForRepresentation:value];
|
|
}
|
|
|
|
if (! objectMapping) continue; // Mapping declined
|
|
NSArray *propertyKeyPaths = [relationshipMapping valueForKeyPath:@"mapping.propertyMappings.sourceKeyPath"];
|
|
if (! RKObjectContainsValueForKeyPaths(value, propertyKeyPaths)) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Track that we applied this mapping
|
|
[mappingsApplied addObject:relationshipMapping];
|
|
|
|
if (value == nil) {
|
|
RKLogDebug(@"Did not find mappable relationship value keyPath '%@'", relationshipMapping.sourceKeyPath);
|
|
|
|
// Optionally nil out the property
|
|
id nilReference = nil;
|
|
if ([self.objectMapping setNilForMissingRelationships] && [self shouldSetValue:&nilReference atKeyPath:relationshipMapping.destinationKeyPath]) {
|
|
RKLogTrace(@"Setting nil for missing relationship value at keyPath '%@'", relationshipMapping.sourceKeyPath);
|
|
[self.destinationObject setValue:nil forKeyPath:relationshipMapping.destinationKeyPath];
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (value == [NSNull null]) {
|
|
RKLogDebug(@"Found null value at keyPath '%@'", relationshipMapping.sourceKeyPath);
|
|
|
|
// Optionally nil out the property
|
|
id nilReference = nil;
|
|
if ([self shouldSetValue:&nilReference atKeyPath:relationshipMapping.destinationKeyPath]) {
|
|
RKLogTrace(@"Setting nil for null relationship value at keyPath '%@'", relationshipMapping.sourceKeyPath);
|
|
[self.destinationObject setValue:nil forKeyPath:relationshipMapping.destinationKeyPath];
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Handle case where incoming content is collection represented by a dictionary
|
|
if (relationshipMapping.mapping.forceCollectionMapping) {
|
|
// If we have forced mapping of a dictionary, map each subdictionary
|
|
if ([value isKindOfClass:[NSDictionary class]]) {
|
|
RKLogDebug(@"Collection mapping forced for NSDictionary, mapping each key/value independently...");
|
|
NSArray *objectsToMap = [NSMutableArray arrayWithCapacity:[value count]];
|
|
for (id key in value) {
|
|
NSDictionary *dictionaryToMap = [NSDictionary dictionaryWithObject:[value valueForKey:key] forKey:key];
|
|
[(NSMutableArray *)objectsToMap addObject:dictionaryToMap];
|
|
}
|
|
value = objectsToMap;
|
|
} else {
|
|
RKLogWarning(@"Collection mapping forced but mappable objects is of type '%@' rather than NSDictionary", NSStringFromClass([value class]));
|
|
}
|
|
}
|
|
|
|
// Handle case where incoming content is a single object, but we want a collection
|
|
Class relationshipClass = [self.objectMapping classForKeyPath:relationshipMapping.destinationKeyPath];
|
|
BOOL mappingToCollection = RKClassIsCollection(relationshipClass);
|
|
if (mappingToCollection && !RKObjectIsCollection(value)) {
|
|
Class orderedSetClass = NSClassFromString(@"NSOrderedSet");
|
|
RKLogDebug(@"Asked to map a single object into a collection relationship. Transforming to an instance of: %@", NSStringFromClass(relationshipClass));
|
|
if ([relationshipClass isSubclassOfClass:[NSArray class]]) {
|
|
value = [relationshipClass arrayWithObject:value];
|
|
} else if ([relationshipClass isSubclassOfClass:[NSSet class]]) {
|
|
value = [relationshipClass setWithObject:value];
|
|
} else if (orderedSetClass && [relationshipClass isSubclassOfClass:orderedSetClass]) {
|
|
value = [relationshipClass orderedSetWithObject:value];
|
|
} else {
|
|
RKLogWarning(@"Failed to transform single object");
|
|
}
|
|
}
|
|
|
|
BOOL setValueForRelationship;
|
|
if (RKObjectIsCollection(value)) {
|
|
setValueForRelationship = [self mapOneToManyRelationshipWithValue:value mapping:relationshipMapping];
|
|
} else {
|
|
setValueForRelationship = [self mapOneToOneRelationshipWithValue:value mapping:relationshipMapping];
|
|
}
|
|
|
|
if (! setValueForRelationship) continue;
|
|
|
|
// Notify the delegate
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didSetValue:forKeyPath:usingMapping:)]) {
|
|
id setValue = [self.destinationObject valueForKeyPath:relationshipMapping.destinationKeyPath];
|
|
[self.delegate mappingOperation:self didSetValue:setValue forKeyPath:relationshipMapping.destinationKeyPath usingMapping:relationshipMapping];
|
|
}
|
|
|
|
// Fail out if a validation error has occurred
|
|
if (self.error) break;
|
|
}
|
|
|
|
return [mappingsApplied count] > 0;
|
|
}
|
|
|
|
- (void)applyNestedMappings
|
|
{
|
|
RKAttributeMapping *attributeMapping = [self.objectMapping attributeMappingForKeyOfRepresentation];
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)cancel
|
|
{
|
|
[super cancel];
|
|
RKLogDebug(@"Mapping operation cancelled: %@", self);
|
|
}
|
|
|
|
- (void)main
|
|
{
|
|
if ([self isCancelled]) return;
|
|
|
|
RKLogDebug(@"Starting mapping operation...");
|
|
RKLogTrace(@"Performing mapping operation: %@", self);
|
|
|
|
if (! self.destinationObject) {
|
|
self.destinationObject = [self destinationObjectForMappingRepresentation:self.sourceObject withMapping:self.mapping];
|
|
if (! self.destinationObject) {
|
|
RKLogDebug(@"Mapping operation failed: Given nil destination object and unable to instantiate a destination object for mapping.");
|
|
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"Cannot perform a mapping operation with a nil destination object." };
|
|
self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorNilDestinationObject userInfo:userInfo];
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Determine the concrete mapping if we were initialized with a dynamic mapping
|
|
if ([self.mapping isKindOfClass:[RKDynamicMapping class]]) {
|
|
self.objectMapping = [(RKDynamicMapping *)self.mapping objectMappingForRepresentation:self.sourceObject];
|
|
if (! self.objectMapping) {
|
|
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"A dynamic mapping failed to return a concrete object mapping matching the representation being mapped." };
|
|
self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorUnableToDetermineMapping userInfo:userInfo];
|
|
return;
|
|
}
|
|
RKLogDebug(@"RKObjectMappingOperation was initialized with a dynamic mapping. Determined concrete mapping = %@", self.objectMapping);
|
|
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didSelectObjectMapping:forDynamicMapping:)]) {
|
|
[self.delegate mappingOperation:self didSelectObjectMapping:self.objectMapping forDynamicMapping:(RKDynamicMapping *)self.mapping];
|
|
}
|
|
} else if ([self.mapping isKindOfClass:[RKObjectMapping class]]) {
|
|
self.objectMapping = (RKObjectMapping *)self.mapping;
|
|
}
|
|
|
|
[self applyNestedMappings];
|
|
if ([self isCancelled]) return;
|
|
BOOL mappedSimpleAttributes = [self applyAttributeMappings:[self simpleAttributeMappings]];
|
|
if ([self isCancelled]) return;
|
|
BOOL mappedRelationships = [[self relationshipMappings] count] ? [self applyRelationshipMappings] : NO;
|
|
if ([self isCancelled]) return;
|
|
// NOTE: We map key path attributes last to allow you to map across the object graphs for objects created/updated by the relationship mappings
|
|
BOOL mappedKeyPathAttributes = [self applyAttributeMappings:[self keyPathAttributeMappings]];
|
|
|
|
if (!mappedSimpleAttributes && !mappedRelationships && !mappedKeyPathAttributes) {
|
|
// We did not find anything to do
|
|
RKLogDebug(@"Mapping operation did not find any mappable values for the attribute and relationship mappings in the given object representation");
|
|
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: @"No mappable values found for any of the attributes or relationship mappings" };
|
|
self.error = [NSError errorWithDomain:RKErrorDomain code:RKMappingErrorUnmappableRepresentation userInfo:userInfo];
|
|
}
|
|
|
|
// We did some mapping work, if there's no error let's commit our changes to the data source
|
|
if (self.error == nil) {
|
|
if ([self.dataSource respondsToSelector:@selector(commitChangesForMappingOperation:error:)]) {
|
|
NSError *error = nil;
|
|
BOOL success = [self.dataSource commitChangesForMappingOperation:self error:&error];
|
|
if (! success) {
|
|
self.error = error;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self.error) {
|
|
if ([self.delegate respondsToSelector:@selector(mappingOperation:didFailWithError:)]) {
|
|
[self.delegate mappingOperation:self didFailWithError:self.error];
|
|
}
|
|
|
|
RKLogError(@"Failed mapping operation: %@", [self.error localizedDescription]);
|
|
} else {
|
|
RKLogDebug(@"Finished mapping operation successfully...");
|
|
}
|
|
}
|
|
|
|
- (BOOL)performMapping:(NSError **)error
|
|
{
|
|
[self start];
|
|
if (error) *error = self.error;
|
|
return self.error == nil;
|
|
}
|
|
|
|
- (NSString *)description
|
|
{
|
|
return [NSString stringWithFormat:@"<%@ %p> for '%@' object. Mapping values from object %@ to object %@ with object mapping %@",
|
|
[self class], self, NSStringFromClass([self.destinationObject class]), self.sourceObject, self.destinationObject, self.objectMapping];
|
|
}
|
|
|
|
@end
|