Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge branch 'release/v0.20.0-rc1'

  • Loading branch information...
commit 016657613e49bf5df3ee567f527ab3452f2997c7 2 parents 22dd9ab + 4407523
@blakewatters blakewatters authored
Showing with 3,674 additions and 1,008 deletions.
  1. +1 −0  Code/CoreData/NSManagedObjectContext+RKAdditions.m
  2. +6 −5 Code/CoreData/RKEntityByAttributeCache.m
  3. +2 −2 Code/CoreData/RKEntityCache.m
  4. +6 −1 Code/CoreData/RKEntityMapping.m
  5. +12 −6 Code/CoreData/RKFetchRequestManagedObjectCache.m
  6. +89 −51 Code/CoreData/RKManagedObjectMappingOperationDataSource.m
  7. +49 −1 Code/CoreData/RKManagedObjectStore.h
  8. +108 −1 Code/CoreData/RKManagedObjectStore.m
  9. +15 −9 Code/CoreData/RKPropertyInspector+CoreData.m
  10. +3 −2 Code/CoreData/RKRelationshipConnectionOperation.m
  11. +4 −0 Code/Network/RKHTTPRequestOperation.h
  12. +51 −23 Code/Network/RKHTTPRequestOperation.m
  13. +10 −1 Code/Network/RKManagedObjectRequestOperation.h
  14. +248 −303 Code/Network/RKManagedObjectRequestOperation.m
  15. +27 −7 Code/Network/RKObjectManager.h
  16. +111 −30 Code/Network/RKObjectManager.m
  17. +1 −1  Code/Network/RKObjectParameterization.m
  18. +24 −2 Code/Network/RKObjectRequestOperation.h
  19. +25 −14 Code/Network/RKObjectRequestOperation.m
  20. +1 −1  Code/Network/RKRequestDescriptor.h
  21. +31 −7 Code/Network/RKResponseMapperOperation.h
  22. +40 −11 Code/Network/RKResponseMapperOperation.m
  23. +1 −1  Code/Network/RKRouter.m
  24. +1 −1  Code/ObjectMapping.h
  25. +2 −2 Code/ObjectMapping/RKAttributeMapping.m
  26. +31 −2 Code/ObjectMapping/RKMapperOperation.h
  27. +41 −17 Code/ObjectMapping/RKMapperOperation.m
  28. +1 −1  Code/ObjectMapping/RKMapperOperation_Private.h
  29. +44 −0 Code/ObjectMapping/RKMappingOperation.h
  30. +128 −37 Code/ObjectMapping/RKMappingOperation.m
  31. +1 −1  Code/ObjectMapping/RKMappingOperationDataSource.h
  32. +1 −1  Code/ObjectMapping/RKObjectMapping.h
  33. +9 −2 Code/ObjectMapping/RKObjectMapping.m
  34. +2 −1  Code/ObjectMapping/RKObjectMappingOperationDataSource.m
  35. +1 −1  Code/ObjectMapping/RKObjectUtilities.m
  36. +2 −2 Code/ObjectMapping/RKPropertyMapping.h
  37. +4 −4 Code/ObjectMapping/RKPropertyMapping.m
  38. +2 −2 Code/ObjectMapping/RKRelationshipMapping.m
  39. +1 −1  Code/Search/RKSearchIndexer.m
  40. +3 −0  Code/Support/RKDictionaryUtilities.m
  41. +1 −1  Code/Support/RKDotNetDateFormatter.m
  42. +3 −2 Code/Support/RKPathMatcher.h
  43. +4 −1 Code/Support/RKPathMatcher.m
  44. +1 −1  Code/Support/RKPathUtilities.m
  45. +1 −1  Code/Testing/RKMappingTest.m
  46. +1 −1  Gemfile
  47. +24 −20 Gemfile.lock
  48. +19 −6 Podfile
  49. +7 −7 Podfile.lock
  50. +42 −5 README.md
  51. +10 −52 Rakefile
  52. +10 −3 RestKit.podspec
  53. +69 −95 RestKit.xcodeproj/project.pbxproj
  54. +1 −1  RestKit.xcworkspace/xcshareddata/xcschemes/Build All Examples.xcscheme
  55. +69 −0 RestKit.xcworkspace/xcshareddata/xcschemes/RestKitFrameworkTests.xcscheme
  56. +69 −0 RestKit.xcworkspace/xcshareddata/xcschemes/RestKitTests.xcscheme
  57. +3 −0  Tests/Fixtures/JSON/humans/empty_human.json
  58. +18 −0 Tests/Fixtures/JSON/humans/has_many_with_to_one_relationship.json
  59. +40 −0 Tests/Fixtures/JSON/lists.json
  60. +4 −34 Tests/Logic/CoreData/RKConnectionDescriptionTest.m
  61. +24 −0 Tests/Logic/CoreData/RKEntityByAttributeCacheTest.m
  62. +47 −0 Tests/Logic/CoreData/RKEntityMappingTest.m
  63. +49 −0 Tests/Logic/CoreData/RKFetchRequestMappingCacheTest.m
  64. +153 −17 Tests/Logic/CoreData/RKManagedObjectMappingOperationDataSourceTest.m
  65. +247 −13 Tests/Logic/CoreData/RKManagedObjectStoreTest.m
  66. +280 −13 Tests/Logic/Network/RKManagedObjectRequestOperationTest.m
  67. +137 −5 Tests/Logic/Network/RKObjectRequestOperationTest.m
  68. +190 −19 Tests/Logic/Network/RKResponseMapperOperationTest.m
  69. +37 −0 Tests/Logic/ObjectMapping/RKMappingOperationTest.m
  70. +426 −4 Tests/Logic/ObjectMapping/RKObjectManagerTest.m
  71. +240 −88 Tests/Logic/ObjectMapping/RKObjectMappingNextGenTest.m
  72. +98 −5 Tests/Logic/ObjectMapping/RKObjectParameterizationTest.m
  73. +23 −17 Tests/Logic/Search/RKSearchIndexerTest.m
  74. +2 −1  Tests/Logic/Search/RKSearchTest.m
  75. +4 −4 Tests/Logic/Support/RKPathMatcherTest.m
  76. BIN  Tests/Models/Data Model.xcdatamodel/elements
  77. BIN  Tests/Models/Data Model.xcdatamodel/layout
  78. +13 −0 Tests/Models/RKPost.h
  79. +24 −0 Tests/Models/RKPost.m
  80. +4 −0 Tests/Models/RKTestUser.h
  81. +1 −1  Tests/Models/RKTestUser.m
  82. +8 −0 Tests/Models/VersionedModel.xcdatamodeld/.xccurrentversion
  83. +15 −0 Tests/Models/VersionedModel.xcdatamodeld/VersionedModel 2.xcdatamodel/contents
  84. +16 −0 Tests/Models/VersionedModel.xcdatamodeld/VersionedModel 3.xcdatamodel/contents
  85. +15 −0 Tests/Models/VersionedModel.xcdatamodeld/VersionedModel 4.xcdatamodel/contents
  86. +14 −0 Tests/Models/VersionedModel.xcdatamodeld/VersionedModel.xcdatamodel/contents
  87. +35 −34 Docs/TESTING.md → Tests/README.md
  88. +18 −0 Tests/RKTestEnvironment.m
  89. +18 −2 Tests/Server/server.rb
  90. +1 −1  VERSION
View
1  Code/CoreData/NSManagedObjectContext+RKAdditions.m
@@ -44,6 +44,7 @@ - (BOOL)saveToPersistentStore:(NSError **)error
__block BOOL success;
[contextToSave performBlockAndWait:^{
success = [contextToSave save:&localError];
+ if (! success && localError == nil) RKLogWarning(@"Saving of managed object context failed, but a `nil` value for the `error` argument was returned. This typically indicates an invalid implementation of a key-value validation method exists within your model. This violation of the API contract may result in the save operation being mis-interpretted by callers that rely on the availability of the error.");
}];
if (! success) {
View
11 Code/CoreData/RKEntityByAttributeCache.m
@@ -226,7 +226,6 @@ - (NSManagedObject *)objectWithAttributeValues:(NSDictionary *)attributeValues i
- (NSSet *)objectsWithAttributeValues:(NSDictionary *)attributeValues inContext:(NSManagedObjectContext *)context
{
- // TODO: Assert that the attribute values contains all of the cache attributes!!!
NSMutableSet *objects = [NSMutableSet set];
NSArray *cacheKeys = RKCacheKeysForEntityFromAttributeValues(self.entity, attributeValues);
for (NSString *cacheKey in cacheKeys) {
@@ -281,10 +280,12 @@ - (void)removeObjectID:(NSManagedObjectID *)objectID forAttributeValues:(NSDicti
{
@synchronized(self.cacheKeysToObjectIDs) {
if (attributeValues && [attributeValues count]) {
- NSString *cacheKey = RKCacheKeyForEntityWithAttributeValues(self.entity, attributeValues);
- NSMutableArray *objectIDs = [self.cacheKeysToObjectIDs objectForKey:cacheKey];
- if (objectIDs && [objectIDs containsObject:objectID]) {
- [objectIDs removeObject:objectID];
+ NSArray *cacheKeys = RKCacheKeysForEntityFromAttributeValues(self.entity, attributeValues);
+ for (NSString *cacheKey in cacheKeys) {
+ NSMutableArray *objectIDs = [self.cacheKeysToObjectIDs objectForKey:cacheKey];
+ if (objectIDs && [objectIDs containsObject:objectID]) {
+ [objectIDs removeObject:objectID];
+ }
}
} else {
RKLogWarning(@"Unable to remove object for object ID %@: empty values dictionary for attributes '%@'", objectID, self.attributes);
View
4 Code/CoreData/RKEntityCache.m
@@ -96,7 +96,7 @@ - (RKEntityByAttributeCache *)attributeCacheForEntity:(NSEntityDescription *)ent
{
NSParameterAssert(entity);
NSParameterAssert(attributeNames);
- for (RKEntityByAttributeCache *cache in self.attributeCaches) {
+ for (RKEntityByAttributeCache *cache in [self.attributeCaches copy]) {
if ([cache.entity isEqual:entity] && [cache.attributes isEqualToArray:attributeNames]) {
return cache;
}
@@ -109,7 +109,7 @@ - (NSSet *)attributeCachesForEntity:(NSEntityDescription *)entity
{
NSAssert(entity, @"Cannot retrieve attribute caches for a nil entity");
NSMutableSet *set = [NSMutableSet set];
- for (RKEntityByAttributeCache *cache in self.attributeCaches) {
+ for (RKEntityByAttributeCache *cache in [self.attributeCaches copy]) {
if ([cache.entity isEqual:entity]) {
[set addObject:cache];
}
View
7 Code/CoreData/RKEntityMapping.m
@@ -149,7 +149,10 @@ + (instancetype)mappingForClass:(Class)objectClass
+ (instancetype)mappingForEntityForName:(NSString *)entityName inManagedObjectStore:(RKManagedObjectStore *)managedObjectStore
{
+ NSParameterAssert(entityName);
+ NSParameterAssert(managedObjectStore);
NSEntityDescription *entity = [[managedObjectStore.managedObjectModel entitiesByName] objectForKey:entityName];
+ NSAssert(entity, @"Unable to find an Entity with the name '%@' in the managed object model", entityName);
return [[self alloc] initWithEntity:entity];
}
@@ -180,8 +183,10 @@ - (id)initWithClass:(Class)objectClass
- (id)copyWithZone:(NSZone *)zone
{
RKEntityMapping *copy = [super copyWithZone:zone];
+ copy.entity = self.entity;
copy.identificationAttributes = self.identificationAttributes;
copy.identificationPredicate = self.identificationPredicate;
+ copy.deletionPredicate = self.deletionPredicate;
for (RKConnectionDescription *connection in self.connections) {
[copy addConnection:[connection copy]];
@@ -235,7 +240,7 @@ - (NSArray *)connections
- (void)addConnectionForRelationship:(id)relationshipOrName connectedBy:(id)connectionSpecifier
{
NSRelationshipDescription *relationship = [relationshipOrName isKindOfClass:[NSRelationshipDescription class]] ? relationshipOrName : [[self.entity relationshipsByName] valueForKey:relationshipOrName];
- NSAssert(relationship, @"No relatiobship was found named '%@' in the '%@' entity", relationshipOrName, [self.entity name]);
+ NSAssert(relationship, @"No relationship was found named '%@' in the '%@' entity", relationshipOrName, [self.entity name]);
RKConnectionDescription *connection = nil;
if ([connectionSpecifier isKindOfClass:[NSString class]]) {
NSString *sourceAttribute = connectionSpecifier;
View
18 Code/CoreData/RKFetchRequestManagedObjectCache.m
@@ -17,13 +17,19 @@
#define RKLogComponent RKlcl_cRestKitCoreData
/*
- NOTE: At the moment this cache key assume that the structure of the values for each key in the `attributeValues` in constant
- i.e. if you have `userID`, it will always be a single value, or `userIDs` will always be an array.
- It will need to be reimplemented if changes in attribute values occur during the life of a single cache
+ This function computes a cache key given a dictionary of attribute values. Each attribute name is used as a fragment within the aggregate cache key. A suffix qualifier is appended that differentiates singular vs. collection attribute values so that '==' and 'IN' predicates are computed appropriately.
*/
-static NSString *RKPredicateCacheKeyForAttributes(NSArray *attributeNames)
+static NSString *RKPredicateCacheKeyForAttributeValues(NSDictionary *attributesValues)
{
- return [[attributeNames sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] componentsJoinedByString:@":"];
+ NSArray *sortedKeys = [[attributesValues allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
+ NSMutableArray *keyFragments = [NSMutableArray array];
+ for (NSString *attributeName in sortedKeys) {
+ id value = [attributesValues objectForKey:attributeName];
+ char suffix = ([value respondsToSelector:@selector(count)]) ? '+' : '.';
+ NSString *attributeKey = [NSString stringWithFormat:@"%@%c", attributeName, suffix];
+ [keyFragments addObject:attributeKey];
+ }
+ return [keyFragments componentsJoinedByString:@":"];
}
// NOTE: We build a dynamic format string here because `NSCompoundPredicate` does not support use of substiution variables
@@ -65,7 +71,7 @@ - (NSSet *)managedObjectsWithEntity:(NSEntityDescription *)entity
NSAssert(attributeValues, @"Cannot retrieve cached objects without attribute values to identify them with.");
NSAssert(managedObjectContext, @"Cannot find existing managed object with a nil context");
- NSString *predicateCacheKey = RKPredicateCacheKeyForAttributes([attributeValues allKeys]);
+ NSString *predicateCacheKey = RKPredicateCacheKeyForAttributeValues(attributeValues);
NSPredicate *substitutionPredicate = [self.predicateCache objectForKey:predicateCacheKey];
if (! substitutionPredicate) {
substitutionPredicate = RKPredicateWithSubsitutionVariablesForAttributeValues(attributeValues);
View
140 Code/CoreData/RKManagedObjectMappingOperationDataSource.m
@@ -54,27 +54,12 @@ static BOOL RKEntityMappingIsIdentifiedByNestingAttribute(RKEntityMapping *entit
return NO;
}
-// We always need to map the dynamic nesting attribute first so that sub-key attribute mappings apply cleanly
-static NSArray *RKEntityIdentificationAttributesInMappingOrder(RKEntityMapping *entityMapping)
-{
- NSMutableArray *orderedAttributes = [NSMutableArray arrayWithCapacity:[[entityMapping identificationAttributes] count]];
- for (NSAttributeDescription *attribute in [entityMapping identificationAttributes]) {
- RKAttributeMapping *attributeMapping = [[entityMapping propertyMappingsByDestinationKeyPath] objectForKey:[attribute name]];
- if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) {
- // We want to map the nesting attribute first
- [orderedAttributes insertObject:attribute atIndex:0];
- } else {
- [orderedAttributes addObject:attribute];
- }
- }
-
- return orderedAttributes;
-}
-
static id RKValueForAttributeMappingInRepresentation(RKAttributeMapping *attributeMapping, NSDictionary *representation)
{
if ([attributeMapping.sourceKeyPath isEqualToString:RKObjectMappingNestingAttributeKeyName]) {
return [[representation allKeys] lastObject];
+ } else if (attributeMapping.sourceKeyPath == nil){
+ return [representation objectForKey:[NSNull null]];
} else {
return [representation valueForKeyPath:attributeMapping.sourceKeyPath];
}
@@ -94,33 +79,64 @@ static id RKValueForAttributeMappingInRepresentation(RKAttributeMapping *attribu
*/
static NSDictionary *RKEntityIdentificationAttributesForEntityMappingWithRepresentation(RKEntityMapping *entityMapping, NSDictionary *representation)
{
+ NSCParameterAssert(entityMapping);
+ NSCAssert([representation isKindOfClass:[NSDictionary class]], @"Expected a dictionary representation");
+
RKDateToStringValueTransformer *dateToStringTransformer = [[RKDateToStringValueTransformer alloc] initWithDateToStringFormatter:entityMapping.preferredDateFormatter
stringToDateFormatters:entityMapping.dateFormatters];
- NSArray *orderedAttributes = RKEntityIdentificationAttributesInMappingOrder(entityMapping);
- BOOL containsNestingAttribute = RKEntityMappingIsIdentifiedByNestingAttribute(entityMapping);
- __block NSArray *attributeMappings = entityMapping.attributeMappings;
- if (containsNestingAttribute) RKLogDebug(@"Detected use of nested dictionary key as identifying attribute");
-
- NSMutableDictionary *entityIdentifierAttributes = [NSMutableDictionary dictionaryWithCapacity:[orderedAttributes count]];
- [orderedAttributes enumerateObjectsUsingBlock:^(NSAttributeDescription *attribute, NSUInteger idx, BOOL *stop) {
+ NSArray *attributeMappings = entityMapping.attributeMappings;
+
+ // If the representation is mapped with a nesting attribute, we must apply the nesting value to the representation before constructing the identification attributes
+ RKAttributeMapping *nestingAttributeMapping = [[entityMapping propertyMappingsBySourceKeyPath] objectForKey:RKObjectMappingNestingAttributeKeyName];
+ if (nestingAttributeMapping) {
+ Class attributeClass = [entityMapping classForProperty:nestingAttributeMapping.destinationKeyPath];
+ id attributeValue = RKTransformedValueWithClass([[representation allKeys] lastObject], attributeClass, dateToStringTransformer);
+ attributeMappings = RKApplyNestingAttributeValueToMappings(nestingAttributeMapping.destinationKeyPath, attributeValue, attributeMappings);
+ }
+
+ // Map the identification attributes
+ NSMutableDictionary *entityIdentifierAttributes = [NSMutableDictionary dictionaryWithCapacity:[entityMapping.identificationAttributes count]];
+ [entityMapping.identificationAttributes enumerateObjectsUsingBlock:^(NSAttributeDescription *attribute, NSUInteger idx, BOOL *stop) {
RKAttributeMapping *attributeMapping = RKAttributeMappingForNameInMappings([attribute name], attributeMappings);
Class attributeClass = [entityMapping classForProperty:[attribute name]];
- id attributeValue = nil;
- if (containsNestingAttribute && idx == 0) {
- // This is the nesting attribute
- attributeValue = RKTransformedValueWithClass([[representation allKeys] lastObject], attributeClass, dateToStringTransformer);
- attributeMappings = RKApplyNestingAttributeValueToMappings([attribute name], attributeValue, attributeMappings);
- } else {
- id sourceValue = RKValueForAttributeMappingInRepresentation(attributeMapping, representation);
- attributeValue = RKTransformedValueWithClass(sourceValue, attributeClass, dateToStringTransformer);
- }
-
+ id sourceValue = RKValueForAttributeMappingInRepresentation(attributeMapping, representation);
+ id attributeValue = RKTransformedValueWithClass(sourceValue, attributeClass, dateToStringTransformer);
[entityIdentifierAttributes setObject:attributeValue ?: [NSNull null] forKey:[attribute name]];
}];
return entityIdentifierAttributes;
}
+static id RKMutableCollectionValueWithObjectForKeyPath(id object, NSString *keyPath)
+{
+ id value = [object valueForKeyPath:keyPath];
+ if ([value isKindOfClass:[NSArray class]]) {
+ return [object mutableArrayValueForKeyPath:keyPath];
+ } else if ([value isKindOfClass:[NSSet class]]) {
+ return [object mutableSetValueForKeyPath:keyPath];
+ } else if ([value isKindOfClass:[NSOrderedSet class]]) {
+ return [object mutableOrderedSetValueForKeyPath:keyPath];
+ } else if (value) {
+ return [NSMutableArray arrayWithObject:value];
+ }
+
+ return nil;
+}
+
+static BOOL RKDeleteInvalidNewManagedObject(NSManagedObject *managedObject)
+{
+ if ([managedObject isKindOfClass:[NSManagedObject class]] && [managedObject managedObjectContext] && [managedObject isNew]) {
+ NSError *validationError = nil;
+ if (! [managedObject validateForInsert:&validationError]) {
+ RKLogDebug(@"Unsaved NSManagedObject failed `validateForInsert:` - Deleting object from context: %@", validationError);
+ [managedObject.managedObjectContext deleteObject:managedObject];
+ return YES;
+ }
+ }
+
+ return NO;
+}
+
@interface RKManagedObjectDeletionOperation : NSOperation
- (id)initWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext;
@@ -210,7 +226,7 @@ - (void)dealloc
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-- (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRepresentation:(NSDictionary *)representation withMapping:(RKObjectMapping *)mapping
+- (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRepresentation:(NSDictionary *)representation withMapping:(RKObjectMapping *)mapping inRelationship:(RKRelationshipMapping *)relationship
{
NSAssert(representation, @"Mappable data cannot be nil");
NSAssert(self.managedObjectContext, @"%@ must be initialized with a managed object context.", [self class]);
@@ -219,16 +235,36 @@ - (id)mappingOperation:(RKMappingOperation *)mappingOperation targetObjectForRep
return [mapping.objectClass new];
}
- RKEntityMapping *entityMapping = (RKEntityMapping *)mapping;
+ RKEntityMapping *entityMapping = (RKEntityMapping *)mapping;
NSDictionary *entityIdentifierAttributes = RKEntityIdentificationAttributesForEntityMappingWithRepresentation(entityMapping, representation);
if (! self.managedObjectCache) {
RKLogWarning(@"Performing managed object mapping with a nil managed object cache:\n"
"Unable to update existing object instances by identification attributes. Duplicate objects may be created.");
}
-
- // If we have found the entity identification attributes, try to find an existing instance to update
+
NSEntityDescription *entity = [entityMapping entity];
NSManagedObject *managedObject = nil;
+
+ // If we are mapping within a relationship, try to find an existing object without identifying attributes
+ if (relationship) {
+ id mutableArrayOrSetValueForExistingObjects = RKMutableCollectionValueWithObjectForKeyPath(mappingOperation.destinationObject, relationship.destinationKeyPath);
+ NSArray *identificationAttributes = [entityMapping.identificationAttributes valueForKey:@"name"];
+ for (NSManagedObject *existingObject in mutableArrayOrSetValueForExistingObjects) {
+ if (! identificationAttributes) {
+ managedObject = existingObject;
+ [mutableArrayOrSetValueForExistingObjects removeObject:managedObject];
+ break;
+ }
+
+ NSDictionary *identificationAttributeValues = [existingObject dictionaryWithValuesForKeys:identificationAttributes];
+ if ([[NSSet setWithArray:[identificationAttributeValues allValues]] isEqualToSet:[NSSet setWithObject:[NSNull null]]]) {
+ managedObject = existingObject;
+ break;
+ }
+ }
+ }
+
+ // If we have found the entity identification attributes, try to find an existing instance to update
if ([entityIdentifierAttributes count]) {
NSSet *objects = [self.managedObjectCache managedObjectsWithEntity:entity
attributeValues:entityIdentifierAttributes
@@ -274,17 +310,7 @@ - (void)emitDeadlockWarningIfNecessary
- (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation error:(NSError **)error
{
if ([mappingOperation.objectMapping isKindOfClass:[RKEntityMapping class]]) {
- [self emitDeadlockWarningIfNecessary];
-
- // Validate unsaved objects
- if ([mappingOperation.destinationObject isKindOfClass:[NSManagedObject class]] && [(NSManagedObject *)mappingOperation.destinationObject isNew]) {
- NSError *validationError = nil;
- if (! [(NSManagedObject *)mappingOperation.destinationObject validateForInsert:&validationError]) {
- RKLogDebug(@"Unsaved NSManagedObject failed `validateForInsert:` - Deleting object from context: %@", validationError);
- [self.managedObjectContext deleteObject:mappingOperation.destinationObject];
- return YES;
- }
- }
+ [self emitDeadlockWarningIfNecessary];
NSArray *connections = [(RKEntityMapping *)mappingOperation.objectMapping connections];
if ([connections count] > 0 && self.managedObjectCache == nil) {
@@ -293,7 +319,16 @@ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation
if (error) *error = localError;
return NO;
}
-
+
+ // Delete the object immediately if there are no connections that may make it valid
+ if ([connections count] == 0 && RKDeleteInvalidNewManagedObject(mappingOperation.destinationObject)) return YES;
+
+ // Attempt to establish the connections and delete the object if its invalid once we are done
+ NSOperationQueue *operationQueue = self.operationQueue ?: [NSOperationQueue currentQueue];
+ NSBlockOperation *deletionOperation = [NSBlockOperation blockOperationWithBlock:^{
+ RKDeleteInvalidNewManagedObject(mappingOperation.destinationObject);
+ }];
+
for (RKConnectionDescription *connection in connections) {
RKRelationshipConnectionOperation *operation = [[RKRelationshipConnectionOperation alloc] initWithManagedObject:mappingOperation.destinationObject connection:connection managedObjectCache:self.managedObjectCache];
[operation setConnectionBlock:^(RKRelationshipConnectionOperation *operation, id connectedValue) {
@@ -308,10 +343,13 @@ - (BOOL)commitChangesForMappingOperation:(RKMappingOperation *)mappingOperation
}
}];
if (self.parentOperation) [operation addDependency:self.parentOperation];
- NSOperationQueue *operationQueue = self.operationQueue ?: [NSOperationQueue currentQueue];
+ [deletionOperation addDependency:operation];
[operationQueue addOperation:operation];
RKLogTrace(@"Enqueued %@ dependent upon parent operation %@ to operation queue %@", operation, self.parentOperation, operationQueue);
}
+
+ // Enqueue our deletion operation for execution after all the connections
+ [operationQueue addOperation:deletionOperation];
// Handle tombstone deletion by predicate
if ([(RKEntityMapping *)mappingOperation.objectMapping deletionPredicate]) {
View
50 Code/CoreData/RKManagedObjectStore.h
@@ -33,7 +33,7 @@
## Managed Object Contexts
- The managed object store provides the application developer with a pair of managed objects with which to work with Core Data. The store configures a primary managed object context with the NSPrivateQueueConcurrencyType that is associated with the persistent store coordinator for handling Core Data persistence. A second context is also created with the NSMainQueueConcurrencyType that is a child of the primary managed object context for doing work on the main queue. Additional child contexts can be created directly or via a convenience method interface provided by the store (see newChildManagedObjectContextWithConcurrencyType:).
+ The managed object store provides the application developer with a pair of managed object contexts with which to work with Core Data. The store configures a primary managed object context with the NSPrivateQueueConcurrencyType that is associated with the persistent store coordinator for handling Core Data persistence. A second context is also created with the NSMainQueueConcurrencyType that is a child of the primary managed object context for doing work on the main queue. Additional child contexts can be created directly or via a convenience method interface provided by the store (see newChildManagedObjectContextWithConcurrencyType:).
The managed object context hierarchy is designed to isolate the main thread from disk I/O and avoid deadlocks. Because the primary context manages its own private queue, saving the main queue context will not result in the objects being saved to the persistent store. The primary context must be saved as well for objects to be persisted to disk.
@@ -244,6 +244,54 @@
*/
- (NSManagedObjectContext *)newChildManagedObjectContextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType;
+///----------------------------
+/// @name Performing Migrations
+///----------------------------
+
+/**
+ Performs a migration on a persistent store at a given URL to the model at the specified URL.
+
+ This method provides support for migrating persistent stores in which the source and destination models have been mutated after being loaded from the model archive on disk, such as when the RestKit managed object searching support is used. In a situation where the persistent store has been created with a dynamically modified managed object model. Core Data is unable to infer the mapping model because the metadata of the persistent store does not agree with that of the managed object model due to the dynamic modifications. In order to perform a migration, one must load the appropriate source model, apply the dynamic changes appropriate for that model, then infer a mapping model from the modified model. This method assists in this process by accepting a source store and a destination model as arguments and searching through all models in the .momd package and yielding each model to the given configuration block for processing. After the block is invoked, the metadata of the store is checked for compatibility with the modified managed object model to identify the source store. Once the source store is found, a mapping model is inferred and the migration proceeds. The migration is done against a copy of the given persistent store and if successful, the migrated store is moved to replace the original store.
+
+ To understand how this is used, consider the following example: Given a managed object model containing two entities 'Article' and 'Tag', the user wishes to configure managed object search indexing on the models and wishes to be able to migrate existing persistent stores across versions. The migration configuration would look something like this:
+
+ NSURL *storeURL = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"MyStore.sqlite"];
+ NSURL *modelURL = [[RKTestFixture fixtureBundle] URLForResource:@"VersionedModel" withExtension:@"momd"];
+ BOOL success = [RKManagedObjectStore migratePersistentStoreOfType:NSSQLiteStoreType atURL:storeURL toModelAtURL:modelURL error:&error configuringModelsWithBlock:^(NSManagedObjectModel *model, NSURL *sourceURL) {
+ // Examine each model and configure search indexing appropriately based on the versionIdentifiers configured in the model
+ if ([[model versionIdentifiers] isEqualToSet:[NSSet setWithObject:@"1.0"]]) {
+ NSEntityDescription *articleEntity = [[model entitiesByName] objectForKey:@"Article"];
+ NSEntityDescription *tagEntity = [[model entitiesByName] objectForKey:@"Tag"];
+ [RKSearchIndexer addSearchIndexingToEntity:articleEntity onAttributes:@[ @"title" ]];
+ [RKSearchIndexer addSearchIndexingToEntity:tagEntity onAttributes:@[ @"name" ]];
+ } else if ([[model versionIdentifiers] isEqualToSet:[NSSet setWithObject:@"2.0"]]) {
+ NSEntityDescription *articleEntity = [[model entitiesByName] objectForKey:@"Article"];
+ NSEntityDescription *tagEntity = [[model entitiesByName] objectForKey:@"Tag"];
+ [RKSearchIndexer addSearchIndexingToEntity:articleEntity onAttributes:@[ @"title", @"body" ]];
+ [RKSearchIndexer addSearchIndexingToEntity:tagEntity onAttributes:@[ @"name" ]];
+ } else if ([[model versionIdentifiers] containsObject:@"3.0"] || [[model versionIdentifiers] containsObject:@"4.0"]) {
+ // We index the same attributes on v3 and v4
+ NSEntityDescription *articleEntity = [[model entitiesByName] objectForKey:@"Article"];
+ NSEntityDescription *tagEntity = [[model entitiesByName] objectForKey:@"Tag"];
+ [RKSearchIndexer addSearchIndexingToEntity:articleEntity onAttributes:@[ @"title", @"body", @"authorName" ]];
+ [RKSearchIndexer addSearchIndexingToEntity:tagEntity onAttributes:@[ @"name" ]];
+ }
+ }];
+
+ @param storeType The type of store that given URL. May be `nil`.
+ @param storeURL A URL to the store that is to be migrated.
+ @param destinationModelURL A URL to the managed object model that the persistent store is to be updated to. This URL may target a specific model version with a .momd package or point to the .momd package itself, in which case the migration is performed to the current version of the model as configured on the .xcdatamodeld file used to the build the .momd package.
+ @param error A pointer to an error object that is set in the event that the migration is unsuccessful.
+ @param block A block object used to configure
+
+ @warning This method is only usable with a versioned Managed Object Model stored as a .momd package containing .mom managed object model archives.
+ */
++ (BOOL)migratePersistentStoreOfType:(NSString *)storeType
+ atURL:(NSURL *)storeURL
+ toModelAtURL:(NSURL *)destinationModelURL
+ error:(NSError **)error
+ configuringModelsWithBlock:(void (^)(NSManagedObjectModel *model, NSURL *sourceURL))block;
+
@end
// Option containing the path to the seed database a SQLite store was initialized with
View
109 Code/CoreData/RKManagedObjectStore.m
@@ -31,6 +31,8 @@
#undef RKLogComponent
#define RKLogComponent RKlcl_cRestKitCoreData
+extern NSString * const RKErrorDomain;
+
NSString * const RKSQLitePersistentStoreSeedDatabasePathOption = @"RKSQLitePersistentStoreSeedDatabasePathOption";
NSString * const RKManagedObjectStoreDidFailSaveNotification = @"RKManagedObjectStoreDidFailSaveNotification";
@@ -52,7 +54,11 @@ + (instancetype)defaultStore
+ (void)setDefaultStore:(RKManagedObjectStore *)managedObjectStore
{
- @synchronized(defaultStore) {
+ if (defaultStore) {
+ @synchronized(defaultStore) {
+ defaultStore = managedObjectStore;
+ }
+ } else {
defaultStore = managedObjectStore;
}
}
@@ -182,6 +188,7 @@ - (void)createManagedObjectContexts
{
NSAssert(!self.persistentStoreManagedObjectContext, @"Unable to create managed object contexts: A primary managed object context already exists.");
NSAssert(!self.mainQueueManagedObjectContext, @"Unable to create managed object contexts: A main queue managed object context already exists.");
+ NSAssert([[self.persistentStoreCoordinator persistentStores] count], @"Cannot create managed object contexts: The persistent store coordinator does not have any persistent stores. This likely means that you forgot to add a persistent store or your attempt to do so failed with an error.");
// Our primary MOC is a private queue concurrency type
self.persistentStoreManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
@@ -288,4 +295,104 @@ - (void)handlePersistentStoreManagedObjectContextDidSaveNotification:(NSNotifica
}];
}
++ (BOOL)migratePersistentStoreOfType:(NSString *)storeType
+ atURL:(NSURL *)storeURL
+ toModelAtURL:(NSURL *)destinationModelURL
+ error:(NSError **)error
+ configuringModelsWithBlock:(void (^)(NSManagedObjectModel *, NSURL *))block
+{
+ BOOL isMomd = [[destinationModelURL pathExtension] isEqualToString:@"momd"]; // Momd contains a directory of versioned models
+ NSManagedObjectModel *destinationModel = [[[NSManagedObjectModel alloc] initWithContentsOfURL:destinationModelURL] mutableCopy];
+
+ // Yield the destination model for configuration (i.e. search indexing)
+ if (block) block(destinationModel, destinationModelURL);
+
+ // Check if the store is compatible with our model
+ NSDictionary *storeMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
+ URL:storeURL
+ error:error];
+ if (! storeMetadata) return NO;
+ if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:storeMetadata]) {
+ // Our store is compatible with the current model, no migration is necessary
+ return YES;
+ }
+
+ RKLogInfo(@"Determined that store at URL %@ has incompatible metadata for managed object model: performing migration...", storeURL);
+
+ NSURL *momdURL = isMomd ? destinationModelURL : [destinationModelURL URLByDeletingLastPathComponent];
+
+ // We can only do migrations within a versioned momd
+ if (![[momdURL pathExtension] isEqualToString:@"momd"]) {
+ NSString *errorDescription = [NSString stringWithFormat:@"Migration failed: Migrations can only be performed to versioned destination models contained in a .momd package. Incompatible destination model given at path '%@'", [momdURL path]];
+ if (error) *error = [NSError errorWithDomain:RKErrorDomain code:NSMigrationError userInfo:@{ NSLocalizedDescriptionKey: errorDescription }];
+ return NO;
+ }
+
+ NSArray *versionedModelURLs = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:momdURL
+ includingPropertiesForKeys:@[] // We only want the URLs
+ options:NSDirectoryEnumerationSkipsPackageDescendants|NSDirectoryEnumerationSkipsHiddenFiles
+ error:error];
+ if (! versionedModelURLs) {
+ return NO;
+ }
+
+ // Iterate across each model version and try to find a compatible store
+ NSManagedObjectModel *sourceModel = nil;
+ for (NSURL *versionedModelURL in versionedModelURLs) {
+ if (! [@[@"mom", @"momd"] containsObject:[versionedModelURL pathExtension]]) continue;
+ NSManagedObjectModel *model = [[[NSManagedObjectModel alloc] initWithContentsOfURL:versionedModelURL] mutableCopy];
+ if (! model) continue;
+ if (block) block(model, versionedModelURL);
+
+ if ([model isConfiguration:nil compatibleWithStoreMetadata:storeMetadata]) {
+ sourceModel = model;
+ break;
+ }
+ }
+
+ // Cannot complete the migration as we can't find a source model
+ if (! sourceModel) {
+ NSString *errorDescription = [NSString stringWithFormat:@"Migration failed: Unable to find the source managed object model used to create the %@ store at path '%@'", storeType, [storeURL path]];
+ if (error) *error = [NSError errorWithDomain:RKErrorDomain code:NSMigrationMissingSourceModelError userInfo:@{ NSLocalizedDescriptionKey: errorDescription }];
+ return NO;
+ }
+
+ // Infer a mapping model and complete the migration
+ NSMappingModel *mappingModel = [NSMappingModel inferredMappingModelForSourceModel:sourceModel
+ destinationModel:destinationModel
+ error:error];
+ if (!mappingModel) {
+ RKLogError(@"Failed to obtain inferred mapping model for source and destination models: aborting migration...");
+ RKLogError(@"%@", *error);
+ return NO;
+ }
+
+ CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
+ NSString *UUID = (__bridge_transfer NSString*)CFUUIDCreateString(kCFAllocatorDefault, uuid);
+ CFRelease(uuid);
+
+ NSString *migrationPath = [NSTemporaryDirectory() stringByAppendingFormat:@"Migration-%@.sqlite", UUID];
+ NSURL *migrationURL = [NSURL fileURLWithPath:migrationPath];
+
+ // Create a migration manager to perform the migration.
+ NSMigrationManager *manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel destinationModel:destinationModel];
+ BOOL success = [manager migrateStoreFromURL:storeURL type:NSSQLiteStoreType
+ options:nil withMappingModel:mappingModel toDestinationURL:migrationURL
+ destinationType:NSSQLiteStoreType destinationOptions:nil error:error];
+
+ if (success) {
+ success = [[NSFileManager defaultManager] removeItemAtURL:storeURL error:error];
+ if (success) {
+ success = [[NSFileManager defaultManager] moveItemAtURL:migrationURL toURL:storeURL error:error];
+ if (success) RKLogInfo(@"Successfully migrated existing store to managed object model at path '%@'...", [destinationModelURL path]);
+ } else {
+ RKLogError(@"Failed to remove existing store at path '%@': unable to complete migration...", [storeURL path]);
+ RKLogError(@"%@", *error);
+ }
+ } else {
+ RKLogError(@"Failed migration with error: %@", *error);
+ }
+ return success;
+}
+
@end
View
24 Code/CoreData/RKPropertyInspector+CoreData.m
@@ -59,14 +59,17 @@ - (NSDictionary *)propertyInspectionForEntity:(NSEntityDescription *)entity
Class managedObjectClass = objc_getClass(className);
objc_property_t prop = class_getProperty(managedObjectClass, propertyName);
-
- const char *attr = property_getAttributes(prop);
- Class destinationClass = RKKeyValueCodingClassFromPropertyAttributes(attr);
- if (destinationClass) {
- NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: name,
- RKPropertyInspectionKeyValueCodingClassKey: destinationClass,
- RKPropertyInspectionIsPrimitiveKey: @(NO) };
- [entityInspection setObject:propertyInspection forKey:name];
+
+ // Property is not defined in the Core Data model -- we cannot infer any details about the destination type
+ if (prop) {
+ const char *attr = property_getAttributes(prop);
+ Class destinationClass = RKKeyValueCodingClassFromPropertyAttributes(attr);
+ if (destinationClass) {
+ NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: name,
+ RKPropertyInspectionKeyValueCodingClassKey: destinationClass,
+ RKPropertyInspectionIsPrimitiveKey: @(NO) };
+ [entityInspection setObject:propertyInspection forKey:name];
+ }
}
}
}
@@ -88,8 +91,11 @@ - (NSDictionary *)propertyInspectionForEntity:(NSEntityDescription *)entity
} else {
NSEntityDescription *destinationEntity = [relationshipDescription destinationEntity];
Class destinationClass = NSClassFromString([destinationEntity managedObjectClassName]);
+ if (! destinationClass) {
+ RKLogWarning(@"Retrieved `Nil` value for class named '%@': This likely indicates that the class is invalid or does not exist in the current target.", [destinationEntity managedObjectClassName]);
+ }
NSDictionary *propertyInspection = @{ RKPropertyInspectionNameKey: name,
- RKPropertyInspectionKeyValueCodingClassKey: destinationClass,
+ RKPropertyInspectionKeyValueCodingClassKey: destinationClass ?: [NSNull null],
RKPropertyInspectionIsPrimitiveKey: @(NO) };
[entityInspection setObject:propertyInspection forKey:name];
}
View
5 Code/CoreData/RKRelationshipConnectionOperation.m
@@ -32,7 +32,8 @@
#undef RKLogComponent
#define RKLogComponent RKlcl_cRestKitCoreData
-static id RKMutableSetValueForRelationship(NSRelationshipDescription *relationship)
+id RKMutableSetValueForRelationship(NSRelationshipDescription *relationship);
+id RKMutableSetValueForRelationship(NSRelationshipDescription *relationship)
{
if (! [relationship isToMany]) return nil;
return [relationship isOrdered] ? [NSMutableOrderedSet orderedSet] : [NSMutableSet set];
@@ -162,7 +163,7 @@ - (id)findConnected:(BOOL *)shouldConnectRelationship
if ([self.connection.relationship isToMany]) {
connectionResult = managedObjects;
} else {
- if ([managedObjects count] > 1) RKLogWarning(@"Retrieved %ld objects satisfying connection criteria for one-to-one relationship connection: only object will be connected.", (long) [managedObjects count]);
+ if ([managedObjects count] > 1) RKLogWarning(@"Retrieved %ld objects satisfying connection criteria for one-to-one relationship connection: only one object will be connected.", (long) [managedObjects count]);
if ([managedObjects count]) connectionResult = [managedObjects anyObject];
}
} else if ([self.connection isKeyPathConnection]) {
View
4 Code/Network/RKHTTPRequestOperation.h
@@ -28,6 +28,10 @@
/**
The `RKHTTPRequestOperation` class is a subclass of `AFHTTPRequestOperation` for HTTP or HTTPS requests made by RestKit. It provides per-instance configuration of the acceptable status codes and content types and integrates with the `RKLog` system to provide detailed requested and response logging. Instances of `RKHTTPRequest` are created by `RKObjectRequestOperation` and its subclasses to HTTP requests that will be object mapped. When used to make standalone HTTP requests, `RKHTTPRequestOperation` instance behave identically to `AFHTTPRequestOperation` with the exception of emitting logging information.
+
+ ## Determining Request Processability
+
+ The `RKHTTPRequestOperation` class diverges from the behavior of `AFHTTPRequestOperation` in the implementation of `canProcessRequest`, which is used to determine if a request can be processed. Because `RKHTTPRequestOperation` handles Content Type and Status Code acceptability at the instance rather than the class level, it by default returns `YES` when sent a `canProcessRequest:` method. Subclasses are encouraged to implement more specific logic if constraining the type of requests handled is desired.
*/
@interface RKHTTPRequestOperation : AFHTTPRequestOperation
View
74 Code/Network/RKHTTPRequestOperation.m
@@ -25,6 +25,8 @@
#import "RKHTTPUtilities.h"
#import "RKMIMETypes.h"
+extern NSString * const RKErrorDomain;
+
// Set Logging Component
#undef RKLogComponent
#define RKLogComponent RKlcl_cRestKitNetwork
@@ -142,7 +144,7 @@ - (void)HTTPOperationDidStart:(NSNotification *)notification
if ((_RKlcl_component_level[(__RKlcl_log_symbol(RKlcl_cRestKitNetwork))]) >= (__RKlcl_log_symbol(RKlcl_vTrace))) {
NSString *body = nil;
if ([operation.request HTTPBody]) {
- body = RKLogTruncateString([NSString stringWithUTF8String:[[operation.request HTTPBody] bytes]]);
+ body = RKLogTruncateString([[NSString alloc] initWithData:[operation.request HTTPBody] encoding:NSUTF8StringEncoding]);
} else if ([operation.request HTTPBodyStream]) {
body = RKStringDescribingStream([operation.request HTTPBodyStream]);
}
@@ -183,48 +185,74 @@ - (void)HTTPOperationDidFinish:(NSNotification *)notification
@end
-@interface AFURLConnectionOperation () <NSURLConnectionDelegate, NSURLConnectionDataDelegate>
+@interface AFURLConnectionOperation ()
+@property (readwrite, nonatomic, strong) NSRecursiveLock *lock;
+@end
+
+@interface RKHTTPRequestOperation ()
@property (readwrite, nonatomic, strong) NSError *HTTPError;
@end
@implementation RKHTTPRequestOperation
-@dynamic HTTPError;
++ (BOOL)canProcessRequest:(NSURLRequest *)request
+{
+ return YES;
+}
+
+// Disable class level Content/Status Code inspection in our superclass
++ (NSSet *)acceptableContentTypes
+{
+ return nil;
+}
+
++ (NSIndexSet *)acceptableStatusCodes
+{
+ return nil;
+}
- (BOOL)hasAcceptableStatusCode
{
- return self.acceptableStatusCodes ? [self.acceptableStatusCodes containsIndex:[self.response statusCode]] : [super hasAcceptableStatusCode];
+ if (! self.response) return NO;
+ NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
+ return self.acceptableStatusCodes ? [self.acceptableStatusCodes containsIndex:statusCode] : [super hasAcceptableStatusCode];
}
- (BOOL)hasAcceptableContentType
{
- return self.acceptableContentTypes ? RKMIMETypeInSet([self.response MIMEType], self.acceptableContentTypes) : [super hasAcceptableContentType];
+ if (! self.response) return NO;
+ NSString *contentType = [self.response MIMEType] ?: @"application/octet-stream";
+ return self.acceptableContentTypes ? RKMIMETypeInSet(contentType, self.acceptableContentTypes) : [super hasAcceptableContentType];
}
+// NOTE: We reimplement this because the AFNetworking implementation keeps Acceptable Status Code/MIME Type at class level
- (NSError *)error
{
- // The first we are invoked, we need to mutate the HTTP error to correct the Content Types and Status Codes returned
- if (self.response && !self.HTTPError) {
- NSError *error = [super error];
- if ([error.domain isEqualToString:AFNetworkingErrorDomain]) {
- if (![self hasAcceptableStatusCode] || ![self hasAcceptableContentType]) {
- NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
-
- if (error.code == NSURLErrorBadServerResponse && ![self hasAcceptableStatusCode]) {
- // Replace the NSLocalizedDescriptionKey
- NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
- [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected status code in (%@), got %d", nil), RKStringFromIndexSet(self.acceptableStatusCodes ?: [NSMutableIndexSet indexSet]), statusCode] forKey:NSLocalizedDescriptionKey];
- self.HTTPError = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo];
- } else if (error.code == NSURLErrorCannotDecodeContentData && ![self hasAcceptableContentType]) {
- // Because we have shifted the Acceptable Content Types and Status Codes
- [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected content type %@, got %@", nil), self.acceptableContentTypes, [self.response MIMEType]] forKey:NSLocalizedDescriptionKey];
- self.HTTPError = [[NSError alloc] initWithDomain:AFNetworkingErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
- }
+ [self.lock lock];
+
+ if (!self.HTTPError && self.response) {
+ if (![self hasAcceptableStatusCode] || ![self hasAcceptableContentType]) {
+ NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
+ [userInfo setValue:self.responseString forKey:NSLocalizedRecoverySuggestionErrorKey];
+ [userInfo setValue:[self.request URL] forKey:NSURLErrorFailingURLErrorKey];
+ [userInfo setValue:self.request forKey:AFNetworkingOperationFailingURLRequestErrorKey];
+ [userInfo setValue:self.response forKey:AFNetworkingOperationFailingURLResponseErrorKey];
+
+ if (![self hasAcceptableStatusCode]) {
+ NSUInteger statusCode = ([self.response isKindOfClass:[NSHTTPURLResponse class]]) ? (NSUInteger)[self.response statusCode] : 200;
+ [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected status code in (%@), got %d", nil), RKStringFromIndexSet(self.acceptableStatusCodes ?: [NSMutableIndexSet indexSet]), statusCode] forKey:NSLocalizedDescriptionKey];
+ self.HTTPError = [[NSError alloc] initWithDomain:RKErrorDomain code:NSURLErrorBadServerResponse userInfo:userInfo];
+ } else if (![self hasAcceptableContentType] && self.response.statusCode != 204) {
+ // NOTE: 204 responses come back as text/plain, which we don't want
+ [userInfo setValue:[NSString stringWithFormat:NSLocalizedString(@"Expected content type %@, got %@", nil), self.acceptableContentTypes, [self.response MIMEType]] forKey:NSLocalizedDescriptionKey];
+ self.HTTPError = [[NSError alloc] initWithDomain:RKErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
}
}
}
- return [super error];
+ NSError *error = self.HTTPError ?: [super error];
+ [self.lock unlock];
+ return error;
}
- (BOOL)wasNotModified
View
11 Code/Network/RKManagedObjectRequestOperation.h
@@ -92,10 +92,19 @@
## 304 'Not Modified' Responses
In the event that a managed object request operation loads a 304 'Not Modified' response for an HTTP request no object mapping is performed as Core Data is assumed to contain a managed object representation of the resource requested. No object mapping is performed on the cached response body, making a cache hit for a managed object request operation a very lightweight operation. To build the mapping result returned to the caller, all of the fetch request blocks matching the request URL will be invoked and each fetch request returned is executed against the managed object context and the objects returned are added to the mapping result. Please note that all managed objects returned in the mapping result for a 'Not Modified' response will be returned under the `[NSNull null]` key path.
+
+ ## Subclassing Notes
+
+ This class relies on the following `RKMapperOperationDelegate` method methods to do its work:
+
+ 1. `mapperDidFinishMapping:`
+
+ If you subclass `RKManagedObjectRequestOperation` and implement any of the above methods then you must call the superclass implementation.
## Limitations and Caveats
- @warning `RKManagedObjectRequestOperation` **does NOT** support object mapping that targets an `NSManagedObjectContext` with a `concurrencyType` of `NSConfinementConcurrencyType`.
+ 1. `RKManagedObjectRequestOperation` **does NOT** support object mapping that targets an `NSManagedObjectContext` with a `concurrencyType` of `NSConfinementConcurrencyType`.
+ 1. `RKManagedObjectRequestOperation` can become deadlocked if configured to perform mapping onto an `NSManagedObjectContext` with the `NSMainQueueConcurrencyType` and is invoked synchronously from the main thread via `start` or an attempt is made to await completion of the operation via `waitUntilFinished`. This occurs because managed object contexts with the `NSMainQueueConcurrencyType` are dependent upon the execution of the main thread's run loop to perform their work and `waitUntilFinished` blocks the calling thread, leading to a deadlock when called from the main thread. Rather than awaiting completion of the operation via `waitUntilFinishes`, consider using a completion block, key-value observation, or spinning the run-loop via `[[NSRunLoop currentRunLoop] runUntilDate:]`.
@see `RKObjectRequestOperation`
@see `RKEntityMapping`
View
551 Code/Network/RKManagedObjectRequestOperation.m
@@ -24,6 +24,8 @@
#import "RKResponseMapperOperation.h"
#import "RKRequestOperationSubclass.h"
#import "NSManagedObjectContext+RKAdditions.h"
+#import "NSManagedObject+RKAdditions.h"
+#import "RKObjectUtilities.h"
// Graph visitor
#import "RKResponseDescriptor.h"
@@ -35,161 +37,209 @@
#undef RKLogComponent
#define RKLogComponent RKlcl_cRestKitCoreData
-@interface RKMappingGraphVisitation : NSObject
-@property (nonatomic, strong) id rootKey; // Will be [NSNull null] or a string value
-@property (nonatomic, strong) NSString *keyPath;
-@property (nonatomic, assign, getter = isCyclic) BOOL cyclic;
-@property (nonatomic, strong) RKMapping *mapping;
+@interface RKEntityMappingEvent : NSObject
+@property (nonatomic, copy) id rootKey;
+@property (nonatomic, copy) NSString *keyPath;
+@property (nonatomic, strong) RKEntityMapping *entityMapping;
+
++ (instancetype)eventWithRootKey:(id)rootKey keyPath:(NSString *)keyPath entityMapping:(RKEntityMapping *)entityMapping;
@end
-@implementation RKMappingGraphVisitation
+@implementation RKEntityMappingEvent
++ (instancetype)eventWithRootKey:(id)rootKey keyPath:(NSString *)keyPath entityMapping:(RKEntityMapping *)entityMapping
+{
+ RKEntityMappingEvent *event = [RKEntityMappingEvent new];
+ event.rootKey = rootKey;
+ event.keyPath = keyPath;
+ event.entityMapping = entityMapping;
+ return event;
+}
- (NSString *)description
{
- return [NSString stringWithFormat:@"<%@: %p rootKey=%@ keyPath=%@ isCylic=%@ mapping=%@>",
- [self class], self, self.rootKey, self.keyPath, self.isCyclic ? @"YES" : @"NO", self.mapping];
+ return [NSString stringWithFormat:@"<%@: %p rootKey=%@ keyPath=%@ entityMapping=%@>",
+ [self class], self, self.rootKey, self.keyPath, self.entityMapping];
}
-
@end
/**
- This class implements Tarjan's algorithm to efficiently visit all nodes within the mapping graph and detect cycles in the graph.
-
- For more details on the algorithm, refer to the Wikipedia page: http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
-
- The following reference implementations were used when building out an Objective-C implementation:
-
- 1. http://algowiki.net/wiki/index.php?title=Tarjan%27s_algorithm
- 1. http://www.logarithmic.net/pfh-files/blog/01208083168/tarjan.py
-
+ Returns the set of keys containing the outermost nesting keypath for all children.
+ For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested'
+ would return: 'this, 'another.one', 'another.two'
*/
-@interface RKNestedManagedObjectKeyPathMappingGraphVisitor : NSObject
-@property (nonatomic, readonly, strong) NSMutableArray *visitations;
-- (id)initWithResponseDescriptors:(NSArray *)responseDescriptors;
-@end
-
-@interface RKNestedManagedObjectKeyPathMappingGraphVisitor ()
-@property (nonatomic, assign) NSUInteger indexCounter;
-@property (nonatomic, strong) NSMutableArray *visitationStack;
-@property (nonatomic, strong) NSMutableDictionary *index;
-@property (nonatomic, strong) NSMutableDictionary *lowLinks;
-@property (nonatomic, strong, readwrite) NSMutableArray *visitations;
-@end
+NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths);
+NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths)
+{
+ return [setOfKeyPaths objectsPassingTest:^BOOL(NSString *keyPath, BOOL *stop) {
+ if ([keyPath isEqual:[NSNull null]]) return YES; // Special case the root key path
+ NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."];
+ NSMutableSet *parentKeyPaths = [NSMutableSet set];
+ for (NSUInteger index = 0; index < [keyPathComponents count] - 1; index++) {
+ [parentKeyPaths addObject:[[keyPathComponents subarrayWithRange:NSMakeRange(0, index + 1)] componentsJoinedByString:@"."]];
+ }
+ for (NSString *parentKeyPath in parentKeyPaths) {
+ if ([setOfKeyPaths containsObject:parentKeyPath]) return NO;
+ }
+ return YES;
+ }];
+}
-@implementation RKNestedManagedObjectKeyPathMappingGraphVisitor
+// Precondition: Must be called from within the correct context
+static NSManagedObject *RKRefetchManagedObjectInContext(NSManagedObject *managedObject, NSManagedObjectContext *managedObjectContext)
+{
+ NSManagedObjectID *managedObjectID = [managedObject objectID];
+ if (! [managedObject managedObjectContext]) return nil; // Object has been deleted
+ if ([managedObjectID isTemporaryID]) {
+ RKLogWarning(@"Unable to refetch managed object %@: the object has a temporary managed object ID.", managedObject);
+ return managedObject;
+ }
+ NSError *error = nil;
+ NSManagedObject *refetchedObject = [managedObjectContext existingObjectWithID:managedObjectID error:&error];
+ NSCAssert(refetchedObject, @"Failed to find existing object with ID %@ in context %@: %@", managedObjectID, managedObjectContext, error);
+ return refetchedObject;
+}
-- (id)initWithResponseDescriptors:(NSArray *)responseDescriptors
+static id RKRefetchedValueInManagedObjectContext(id value, NSManagedObjectContext *managedObjectContext)
{
- self = [self init];
- if (self) {
- self.indexCounter = 0;
- self.visitationStack = [NSMutableArray array];
- self.index = [NSMutableDictionary dictionary];
- self.lowLinks = [NSMutableDictionary dictionary];
- self.visitations = [NSMutableArray array];
-
- for (RKResponseDescriptor *responseDescriptor in responseDescriptors) {
- self.indexCounter = 0;
- [self.visitationStack removeAllObjects];
- [self.index removeAllObjects];
- [self.lowLinks removeAllObjects];
- [self visitMapping:responseDescriptor.mapping atKeyPath:responseDescriptor.keyPath];
+ if (! value) {
+ return value;
+ } else if ([value isKindOfClass:[NSArray class]]) {
+ BOOL isMutable = [value isKindOfClass:[NSMutableArray class]];
+ NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]];
+ for (__strong id object in value) {
+ if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
+ if (object) [newValue addObject:object];
+ }
+ value = (isMutable) ? newValue : [newValue copy];
+ } else if ([value isKindOfClass:[NSSet class]]) {
+ BOOL isMutable = [value isKindOfClass:[NSMutableSet class]];
+ NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]];
+ for (__strong id object in value) {
+ if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
+ if (object) [newValue addObject:object];
}
+ value = (isMutable) ? newValue : [newValue copy];
+ } else if ([value isKindOfClass:[NSOrderedSet class]]) {
+ BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]];
+ NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet];
+ [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
+ if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
+ if (object) [newValue setObject:object atIndex:index];
+ }];
+ value = (isMutable) ? newValue : [newValue copy];
+ } else if ([value isKindOfClass:[NSManagedObject class]]) {
+ value = RKRefetchManagedObjectInContext(value, managedObjectContext);
}
+ return value;
+}
+
+/**
+ This is an NSProxy object that stands in for the mapping result and provides support for refetching the results on demand. This enables us to defer the refetching until someone accesses the results directly. For managed object request operations that do not use the mapping result (such as those used in conjunction with a NSFetchedResultsController), the refetching will be skipped entirely.
+ */
+@interface RKRefetchingMappingResult : NSProxy
+
+- (id)initWithMappingResult:(RKMappingResult *)mappingResult
+ managedObjectContext:(NSManagedObjectContext *)managedObjectContext
+ entityMappingEvents:(NSArray *)entityMappingEvents;
+@end
+
+@interface RKRefetchingMappingResult ()
+@property (nonatomic, strong) RKMappingResult *mappingResult;
+@property (nonatomic, strong) NSManagedObjectContext *managedObjectContext;
+@property (nonatomic, strong) NSArray *entityMappingEvents;
+@property (nonatomic, assign) BOOL refetched;
+@end
+
+@implementation RKRefetchingMappingResult
+
++ (NSString *)description
+{
+ return [[super description] stringByAppendingString:@"_RKRefetchingMappingResult"];
+}
+
+- (id)initWithMappingResult:(RKMappingResult *)mappingResult
+ managedObjectContext:(NSManagedObjectContext *)managedObjectContext
+ entityMappingEvents:(NSArray *)entityMappingEvents;
+{
+ self.mappingResult = mappingResult;
+ self.managedObjectContext = managedObjectContext;
+ self.entityMappingEvents = entityMappingEvents;
return self;
}
-- (RKMappingGraphVisitation *)visitationForMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
+- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
- RKMappingGraphVisitation *visitation = [RKMappingGraphVisitation new];
- visitation.mapping = mapping;
- if ([self.visitationStack count] == 0) {
- // If we are the first item in the stack, we are visiting the rootKey
- visitation.rootKey = keyPath ?: [NSNull null];
- } else {
- // Take the root key from the visitation stack
- visitation.rootKey = [[self.visitationStack objectAtIndex:0] rootKey];
- visitation.keyPath = keyPath;
- }
-
- return visitation;
+ return [self.mappingResult methodSignatureForSelector:selector];
}
-// Traverse the mappings graph using Tarjan's algorithm
-- (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
+- (void)forwardInvocation:(NSInvocation *)invocation
{
- // Track the visit to each node in the graph. Note that we do not pop the stack as we traverse back up
- NSValue *dictionaryKey = [NSValue valueWithNonretainedObject:mapping];
- [self.index setObject:@(self.indexCounter) forKey:dictionaryKey];
- [self.lowLinks setObject:@(self.indexCounter) forKey:dictionaryKey];
- self.indexCounter++;
-
- RKMappingGraphVisitation *visitation = [self visitationForMapping:mapping atKeyPath:keyPath];
- [self.visitationStack addObject:visitation];
+ if (! self.refetched) {
+ self.mappingResult = [self refetchedMappingResult];
+ self.refetched = YES;
+ }
+ [invocation invokeWithTarget:self.mappingResult];
+}
+
+- (NSString *)description
+{
+ return [self.mappingResult description];
+}
+
+- (RKMappingResult *)refetchedMappingResult
+{
+ NSAssert(!self.refetched, @"Mapping result should only be refetched once");
+ if (! [self.mappingResult count]) return self.mappingResult;
- if ([mapping isKindOfClass:[RKObjectMapping class]]) {
- RKObjectMapping *objectMapping = (RKObjectMapping *)mapping;
- for (RKRelationshipMapping *relationshipMapping in objectMapping.relationshipMappings) {
- // Check if the successor relationship appears in the lowlinks
- NSValue *relationshipKey = [NSValue valueWithNonretainedObject:relationshipMapping.mapping];
- NSNumber *relationshipLowValue = [self.lowLinks objectForKey:relationshipKey];
- NSString *nestedKeyPath = ([self.visitationStack count] > 1 && keyPath) ? [@[ keyPath, relationshipMapping.destinationKeyPath ] componentsJoinedByString:@"."] : relationshipMapping.destinationKeyPath;
- if (relationshipLowValue == nil) {
- // The relationship has not yet been visited, recurse
- [self visitMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
-
- // Set the lowlink value for parent mapping to the lower value for us or the child mapping we just recursed on
- NSNumber *lowLinkForMapping = [self.lowLinks objectForKey:dictionaryKey];
- NSNumber *lowLinkForSuccessor = [self.lowLinks objectForKey:relationshipKey];
-
- if ([lowLinkForMapping compare:lowLinkForSuccessor] == NSOrderedDescending) {
- [self.lowLinks setObject:lowLinkForSuccessor forKey:dictionaryKey];
- }
- } else {
- // The child mapping is already in the stack, so it is part of a strongly connected component
- NSNumber *lowLinkForMapping = [self.lowLinks objectForKey:dictionaryKey];
- NSNumber *indexValueForSuccessor = [self.index objectForKey:relationshipKey];
- if ([lowLinkForMapping compare:indexValueForSuccessor] == NSOrderedDescending) {
- [self.lowLinks setObject:indexValueForSuccessor forKey:dictionaryKey];
- }
-
- // Since this mapping already appears in lowLinks, we have a cycle at this point in the graph
- if ([relationshipMapping.mapping isKindOfClass:[RKEntityMapping class]]) {
- RKMappingGraphVisitation *cyclicVisitation = [self visitationForMapping:relationshipMapping.mapping atKeyPath:nestedKeyPath];
- cyclicVisitation.cyclic = YES;
- [self.visitations addObject:cyclicVisitation];
+ NSMutableDictionary *newDictionary = [self.mappingResult.dictionary mutableCopy];
+ [self.managedObjectContext performBlockAndWait:^{
+ NSSet *rootKeys = [NSSet setWithArray:[self.entityMappingEvents valueForKey:@"rootKey"]];
+ for (id rootKey in rootKeys) {
+ NSArray *eventsForRootKey = [self.entityMappingEvents filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"rootKey = %@", rootKey]];
+ NSSet *keyPaths = [NSSet setWithArray:[eventsForRootKey valueForKey:@"keyPath"]];
+ // If keyPaths contains null, then the root object is a managed object and we only need to refetch it
+ NSSet *nonNestedKeyPaths = ([keyPaths containsObject:[NSNull null]]) ? [NSSet setWithObject:[NSNull null]] : RKSetByRemovingSubkeypathsFromSet(keyPaths);
+
+ NSDictionary *mappingResultsAtRootKey = [newDictionary objectForKey:rootKey];
+ for (NSString *keyPath in nonNestedKeyPaths) {
+ id value = nil;
+ if ([keyPath isEqual:[NSNull null]]) {
+ value = RKRefetchedValueInManagedObjectContext(mappingResultsAtRootKey, self.managedObjectContext);
+ if (value) [newDictionary setObject:value forKey:rootKey];
+ } else {
+ NSMutableArray *keyPathComponents = [[keyPath componentsSeparatedByString:@"."] mutableCopy];
+ NSString *destinationKey = [keyPathComponents lastObject];
+ [keyPathComponents removeLastObject];
+ id sourceObject = [keyPathComponents count] ? [mappingResultsAtRootKey valueForKeyPath:[keyPathComponents componentsJoinedByString:@"."]] : mappingResultsAtRootKey;
+ if (RKObjectIsCollection(sourceObject)) {
+ // This is a to-many relationship, we want to refetch each item at the keyPath
+ for (id nestedObject in sourceObject) {
+ // Refetch this object. Set it on the destination.
+ NSManagedObject *managedObject = [nestedObject valueForKey:destinationKey];
+ [nestedObject setValue:RKRefetchedValueInManagedObjectContext(managedObject, self.managedObjectContext) forKey:destinationKey];
+ }
+ } else {
+ // This is a singular relationship. We want to refetch the object and set it directly.
+ id valueToRefetch = [sourceObject valueForKey:destinationKey];
+ [sourceObject setValue:RKRefetchedValueInManagedObjectContext(valueToRefetch, self.managedObjectContext) forKey:destinationKey];
+ }
}
}
}
- } else if ([mapping isKindOfClass:[RKDynamicMapping class]]) {
- // Pop the dynamic mapping off of the stack so that our children are rooted at the same level
- [self.visitationStack removeLastObject];
-
- // Dynamic mappings appear at the same point in the graph, so we recurse with the same keyPath
- for (RKMapping *nestedMapping in [(RKDynamicMapping *)mapping objectMappings]) {
- [self visitMapping:nestedMapping atKeyPath:keyPath];
- }
- }
+ }];
- // If the current mapping is a root node, then pop the stack to create an SCC
- NSNumber *lowLinkValueForMapping = [self.lowLinks objectForKey:dictionaryKey];
- NSNumber *indexValueForMapping = [self.index objectForKey:dictionaryKey];
- if ([lowLinkValueForMapping isEqualToNumber:indexValueForMapping]) {
- NSUInteger index = [self.visitationStack indexOfObject:visitation];
- if (index != NSNotFound) {
- NSRange removalRange = NSMakeRange(index, [self.visitationStack count] - index);
- [self.visitationStack removeObjectsInRange:removalRange];
- }
-
- if ([visitation.mapping isKindOfClass:[RKEntityMapping class]]) {
- [self.visitations addObject:visitation];
- }
- }
+ return [[RKMappingResult alloc] initWithDictionary:newDictionary];
}
@end
+static NSString *RKKeyPathByDeletingLastComponent(NSString *keyPath)
+{
+ NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."];
+ return ([keyPathComponents count] > 1) ? [[keyPathComponents subarrayWithRange:NSMakeRange(0, [keyPathComponents count] - 1)] componentsJoinedByString:@"."] : nil;
+}
+
NSArray *RKArrayOfFetchRequestFromBlocksWithURL(NSArray *fetchRequestBlocks, NSURL *URL)
{
NSMutableArray *fetchRequests = [NSMutableArray array];
@@ -225,132 +275,6 @@ - (void)visitMapping:(RKMapping *)mapping atKeyPath:(NSString *)keyPath
return mutableSet;
}
-/**
- Traverses a set of cyclic key paths within the mapping result. Because these relationships are cyclic, we continue collecting managed objects and traversing until the values returned by the key path are a complete subset of all objects already in the set.
- */
-static void RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(id graph, NSSet *cyclicKeyPaths, NSMutableSet *mutableSet)
-{
- if ([graph respondsToSelector:@selector(count)] && [graph count] == 0) return;
-
- for (NSString *cyclicKeyPath in cyclicKeyPaths) {
- NSSet *objectsAtCyclicKeyPath = RKFlattenCollectionToSet([graph valueForKeyPath:cyclicKeyPath]);
- if ([objectsAtCyclicKeyPath count] == 0 || [objectsAtCyclicKeyPath isEqualToSet:[NSSet setWithObject:[NSNull null]]]) continue;
- if (! [objectsAtCyclicKeyPath isSubsetOfSet:mutableSet]) {
- [mutableSet unionSet:objectsAtCyclicKeyPath];
- for (id nestedValue in objectsAtCyclicKeyPath) {
- RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(nestedValue, cyclicKeyPaths, mutableSet);
- }
- }
- }
-}
-
-/**
- Returns the set of keys containing the outermost nesting keypath for all children.
- For example, given a set containing: 'this', 'this.that', 'another.one.test', 'another.two.test', 'another.one.test.nested'
- would return: 'this, 'another.one', 'another.two'
- */
-NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths);
-NSSet *RKSetByRemovingSubkeypathsFromSet(NSSet *setOfKeyPaths)
-{
- return [setOfKeyPaths objectsPassingTest:^BOOL(NSString *keyPath, BOOL *stop) {
- if ([keyPath isEqual:[NSNull null]]) return YES; // Special case the root key path
- NSArray *keyPathComponents = [keyPath componentsSeparatedByString:@"."];
- NSMutableSet *parentKeyPaths = [NSMutableSet set];
- for (NSUInteger index = 0; index < [keyPathComponents count] - 1; index++) {
- [parentKeyPaths addObject:[[keyPathComponents subarrayWithRange:NSMakeRange(0, index + 1)] componentsJoinedByString:@"."]];
- }
- for (NSString *parentKeyPath in parentKeyPaths) {
- if ([setOfKeyPaths containsObject:parentKeyPath]) return NO;
- }
- return YES;
- }];
-}
-
-static void RKSetMappedValueForKeyPathInDictionary(id value, id rootKey, NSString *keyPath, NSMutableDictionary *dictionary)
-{
- NSCParameterAssert(value);
- NSCParameterAssert(rootKey);
- NSCParameterAssert(dictionary);
- if (keyPath && ![keyPath isEqual:[NSNull null]]) {
- id valueAtRootKey = [dictionary objectForKey:rootKey];
- [valueAtRootKey setValue:value forKeyPath:keyPath];
- } else {
- [dictionary setObject:value forKey:rootKey];
- }
-}
-
-// Precondition: Must be called from within the correct context
-static NSManagedObject *RKRefetchManagedObjectInContext(NSManagedObject *managedObject, NSManagedObjectContext *managedObjectContext)
-{
- NSManagedObjectID *managedObjectID = [managedObject objectID];
- if (! [managedObject managedObjectContext]) return nil; // Object has been deleted
- if ([managedObjectID isTemporaryID]) {
- RKLogWarning(@"Unable to refetch managed object %@: the object has a temporary managed object ID.", managedObject);
- return managedObject;
- }
- NSError *error = nil;
- NSManagedObject *refetchedObject = [managedObjectContext existingObjectWithID:managedObjectID error:&error];
- NSCAssert(refetchedObject, @"Failed to find existing object with ID %@ in context %@: %@", managedObjectID, managedObjectContext, error);
- return refetchedObject;
-}
-
-// Finds the key paths for all entity mappings in the graph whose parent objects are not other managed objects
-static NSDictionary *RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext(NSDictionary *dictionaryOfManagedObjects, NSArray *visitations, NSManagedObjectContext *managedObjectContext)
-{
- if (! [dictionaryOfManagedObjects count]) return dictionaryOfManagedObjects;
-
- NSMutableDictionary *newDictionary = [dictionaryOfManagedObjects mutableCopy];
- [managedObjectContext performBlockAndWait:^{
- NSSet *rootKeys = [NSSet setWithArray:[visitations valueForKey:@"rootKey"]];
- for (id rootKey in rootKeys) {
- NSArray *visitationsForRootKey = [visitations filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"rootKey = %@", rootKey]];
- NSSet *keyPaths = [visitationsForRootKey valueForKey:@"keyPath"];
- // If keyPaths contains null, then the root object is a managed object and we only need to refetch it
- NSSet *nonNestedKeyPaths = ([keyPaths containsObject:[NSNull null]]) ? [NSSet setWithObject:[NSNull null]] : RKSetByRemovingSubkeypathsFromSet(keyPaths);
-
- NSDictionary *mappingResultsAtRootKey = [dictionaryOfManagedObjects objectForKey:rootKey];
- for (NSString *keyPath in nonNestedKeyPaths) {
- id value = [keyPath isEqual:[NSNull null]] ? mappingResultsAtRootKey : [mappingResultsAtRootKey valueForKeyPath:keyPath];
- if (! value) {
- continue;
- } else if ([value isKindOfClass:[NSArray class]]) {
- BOOL isMutable = [value isKindOfClass:[NSMutableArray class]];
- NSMutableArray *newValue = [[NSMutableArray alloc] initWithCapacity:[value count]];
- for (__strong id object in value) {
- if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
- if (object) [newValue addObject:object];
- }
- value = (isMutable) ? newValue : [newValue copy];
- } else if ([value isKindOfClass:[NSSet class]]) {
- BOOL isMutable = [value isKindOfClass:[NSMutableSet class]];
- NSMutableSet *newValue = [[NSMutableSet alloc] initWithCapacity:[value count]];
- for (__strong id object in value) {
- if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
- if (object) [newValue addObject:object];
- }
- value = (isMutable) ? newValue : [newValue copy];
- } else if ([value isKindOfClass:[NSOrderedSet class]]) {
- BOOL isMutable = [value isKindOfClass:[NSMutableOrderedSet class]];
- NSMutableOrderedSet *newValue = [NSMutableOrderedSet orderedSet];
- [(NSOrderedSet *)value enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
- if ([object isKindOfClass:[NSManagedObject class]]) object = RKRefetchManagedObjectInContext(object, managedObjectContext);
- if (object) [newValue setObject:object atIndex:index];
- }];
- value = (isMutable) ? newValue : [newValue copy];
- } else if ([value isKindOfClass:[NSManagedObject class]]) {
- value = RKRefetchManagedObjectInContext(value, managedObjectContext);
- }
-
- if (value) {
- RKSetMappedValueForKeyPathInDictionary(value, rootKey, keyPath, newDictionary);
- }
- }
- }
- }];
-
- return newDictionary;
-}
-
static NSURL *RKRelativeURLFromURLAndResponseDescriptors(NSURL *URL, NSArray *responseDescriptors)
{
NSCParameterAssert(URL);
@@ -373,6 +297,7 @@ @interface RKManagedObjectRequestOperation ()
@property (nonatomic, strong, readwrite) NSError *error;
@property (nonatomic, strong, readwrite) RKMappingResult *mappingResult;
@property (nonatomic, copy) id (^willMapDeserializedResponseBlock)(id deserializedResponseBody);
+@property (nonatomic, strong) NSArray *entityMappingEvents;
@end
@implementation RKManagedObjectRequestOperation
@@ -395,7 +320,14 @@ - (void)setTargetObject:(id)targetObject
[super setTargetObject:targetObject];
if ([targetObject isKindOfClass:[NSManagedObject class]]) {
+ if ([[targetObject objectID] isTemporaryID]) {
+ NSError *error = nil;
+ BOOL success = [[targetObject managedObjectContext] obtainPermanentIDsForObjects:@[ targetObject ] error:&error];
+ if (! success) RKLogWarning(@"Failed to obtain permanent objectID for targetObject: %@ (%ld)", [error localizedDescription], (long) error.code);
+ }
self.targetObjectID = [targetObject objectID];
+ } else {
+ self.targetObjectID = nil;
}
}
@@ -406,10 +338,9 @@ - (void)setManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
if (managedObjectContext) {
// Create a private context
NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
- [privateContext performBlockAndWait:^{
- privateContext.parentContext = managedObjectContext;
- privateContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
- }];
+ [privateContext setParentContext:managedObjectContext];
+ [privateContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
+
self.privateContext = privateContext;
} else {
self.privateContext = nil;
@@ -445,10 +376,12 @@ - (RKMappingResult *)performMappingOnResponse:(NSError **)error
return [[RKMappingResult alloc] initWithDictionary:@{ [NSNull null]: managedObjects }];
}
- self.responseMapperOperation = [[RKManagedObjectResponseMapperOperation alloc] initWithResponse:self.HTTPRequestOperation.response
- data:self.HTTPRequestOperation.responseData
- responseDescriptors:self.responseDescriptors];
+ self.responseMapperOperation = [[RKManagedObjectResponseMapperOperation alloc] initWithRequest:self.HTTPRequestOperation.request
+ response:self.HTTPRequestOperation.response
+ data:self.HTTPRequestOperation.responseData
+ responseDescriptors:self.responseDescriptors];
self.responseMapperOperation.mapperDelegate = self;
+ self.responseMapperOperation.mappingMetadata = self.mappingMetadata;
self.responseMapperOperation.targetObjectID = self.targetObjectID;
self.responseMapperOperation.managedObjectContext = self.privateContext;
self.responseMapperOperation.managedObjectCache = self.managedObjectCache;
@@ -523,7 +456,7 @@ - (NSSet *)localObjectsFromFetchRequestsMatchingRequestURL:(NSError **)error
return localObjects;
}
-- (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result withVisitor:(RKNestedManagedObjectKeyPathMappingGraphVisitor *)visitor error:(NSError **)error
+- (BOOL)deleteLocalObjectsMissingFromMappingResult:(NSError **)error
{
if (! self.deletesOrphanedObjects) {
RKLogDebug(@"Skipping deletion of orphaned objects: disabled as deletesOrphanedObjects=NO");
@@ -542,28 +475,13 @@ - (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result wit
// Build an aggregate collection of all the managed objects in the mapping result
NSMutableSet *managedObjectsInMappingResult = [NSMutableSet set];
- NSDictionary *mappingResultDictionary = result.dictionary;
+ NSDictionary *mappingResultDictionary = self.mappingResult.dictionary;
- for (RKMappingGraphVisitation *visitation in visitor.visitations) {
- id objectsAtRoot = [mappingResultDictionary objectForKey:visitation.rootKey];
- id managedObjects = nil;
- @try {
- managedObjects = visitation.keyPath ? [objectsAtRoot valueForKeyPath:visitation.keyPath] : objectsAtRoot;
- }
- @catch (NSException *exception) {
- if ([exception.name isEqualToString:NSUndefinedKeyException]) {
- RKLogWarning(@"Caught undefined key exception for keyPath '%@' in mapping result: This likely indicates an ambiguous keyPath is used across response descriptor or dynamic mappings.", visitation.keyPath);
- continue;
- }
- [exception raise];
- }
- [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)];
-
- if (visitation.isCyclic) {
- NSSet *cyclicKeyPaths = [NSSet setWithArray:[visitation valueForKeyPath:@"mapping.relationshipMappings.destinationKeyPath"]];
- [managedObjectsInMappingResult unionSet:RKFlattenCollectionToSet(managedObjects)];
- RKAddObjectsInGraphWithCyclicKeyPathsToMutableSet(managedObjects, cyclicKeyPaths, managedObjectsInMappingResult);
- }
+ for (RKEntityMappingEvent *event in self.entityMappingEvents) {
+ id objectsAtRoot = [mappingResultDictionary objectForKey:event.rootKey];
+ id managedObjects = event.keyPath ? [objectsAtRoot valueForKeyPath:event.keyPath] : objectsAtRoot;
+ NSSet *flattenedSet = RKFlattenCollectionToSet(managedObjects);
+ [managedObjectsInMappingResult unionSet:flattenedSet];
}
NSSet *localObjects = [self localObjectsFromFetchRequestsMatchingRequestURL:error];
@@ -581,35 +499,45 @@ - (BOOL)deleteLocalObjectsMissingFromMappingResult:(RKMappingResult *)result wit
return YES;
}
-- (BOOL)saveContext:(NSError **)error
+- (BOOL)saveContext:(NSManagedObjectContext *)context error:(NSError **)error
{
__block BOOL success = YES;
__block NSError *localError = nil;
- if ([self.privateContext hasChanges]) {
- if (self.savesToPersistentStore) {
- success = [self.privateContext saveToPersistentStore:&localError];
- } else {
- [self.privateContext performBlockAndWait:^{
- success = [self.privateContext save:&localError];
+ if (self.savesToPersistentStore) {
+ success = [context saveToPersistentStore:&localError];
+ } else {
+ [context performBlockAndWait:^{
+ success = [context save:&localError];
+ }];
+ }
+ if (success) {
+ if ([self.targetObject isKindOfClass:[NSManagedObject class]]) {
+ [self.managedObjectContext performBlock:^{
+ RKLogDebug(@"Refreshing mapped target object %@ in context %@", self.targetObject, self.managedObjectContext);
+ [self.managedObjectContext refreshObject:self.targetObject mergeChanges:YES];
}];
}
- if (success) {
- if ([self.targetObject isKindOfClass:[NSManagedObject class]]) {
- [self.managedObjectContext performBlock:^{
- RKLogDebug(@"Refreshing mapped target object %@ in context %@", self.targetObject, self.managedObjectContext);
- [self.managedObjectContext refreshObject:self.targetObject mergeChanges:YES];
- }];
- }
- } else {
- if (error) *error = localError;
- RKLogError(@"Failed saving managed object context %@ %@", (self.savesToPersistentStore ? @"to the persistent store" : @""), self.privateContext);
- RKLogCoreDataError(localError);
- }
+ } else {
+ if (error) *error = localError;
+ RKLogError(@"Failed saving managed object context %@ %@", (self.savesToPersistentStore ? @"to the persistent store" : @""), context);
+ RKLogCoreDataError(localError);
}
return success;
}
+- (BOOL)saveContext:(NSError **)error
+{
+ if ([self.privateContext hasChanges]) {
+ return [self saveContext:self.privateContext error:error];
+ } else if ([self.targetObject isKindOfClass:[NSManagedObject class]] && [(NSManagedObject *)self.targetObject isNew]) {
+ // Object was like POST'd in an unsaved state and we wish to persist
+ return [self saveContext:[self.targetObject managedObjectContext] error:error];
+ }
+
+ return YES;
+}
+
- (BOOL)obtainPermanentObjectIDsForInsertedObjects:(NSError **)error
{
__block BOOL _blockSuccess = YES;
@@ -631,9 +559,6 @@ - (void)willFinish
BOOL success;
NSError *error = nil;
- // Construct a set of key paths to all of the managed objects in the mapping result
- RKNestedManagedObjectKeyPathMappingGraphVisitor *visitor = [[RKNestedManagedObjectKeyPathMappingGraphVisitor alloc] initWithResponseDescriptors:self.responseMapperOperation.matchingResponseDescriptors];
-
// Handle any cleanup
success = [self deleteTargetObjectIfAppropriate:&error];
if (! success) {
@@ -641,7 +566,7 @@ - (void)willFinish
return;
}
- success = [self deleteLocalObjectsMissingFromMappingResult:self.mappingResult withVisitor:visitor error:&error];
+ success = [self deleteLocalObjectsMissingFromMappingResult:&error];
if (! success) {
self.error = error;
return;
@@ -661,9 +586,29 @@ - (void)willFinish
// Refetch all managed objects nested at key paths within the results dictionary before returning
if (self.mappingResult) {
- NSDictionary *resultsDictionaryFromOriginalContext = RKDictionaryFromDictionaryWithManagedObjectsInVisitationsRefetchedInContext([self.mappingResult dictionary], visitor.visitations, self.managedObjectContext);
- self.mappingResult = [[RKMappingResult alloc] initWithDictionary:resultsDictionaryFromOriginalContext];
+ self.mappingResult = (RKMappingResult *)[[RKRefetchingMappingResult alloc] initWithMappingResult:self.mappingResult managedObjectContext:self.managedObjectContext entityMappingEvents:self.entityMappingEvents];
}
}
+- (void)mapperDidFinishMapping:(RKMapperOperation *)mapper
+{
+ NSMutableArray *entityMappingEvents = [NSMutableArray array];
+ [mapper.mappingInfo enumerateKeysAndObjectsUsingBlock:^(id rootKey, NSDictionary *keyPathsToPropertyMappings, BOOL *stop) {
+ [keyPathsToPropertyMappings enumerateKeysAndObjectsUsingBlock:^(NSString *keyPath, RKPropertyMapping *propertyMapping, BOOL *stop) {
+ if ([propertyMapping.objectMapping isKindOfClass:[RKEntityMapping class]]) {
+ // If the parent object mapping is an `RKEntityMapping`, add a mapping event at its keyPath
+ [entityMappingEvents addObject:[RKEntityMappingEvent eventWithRootKey:rootKey
+ keyPath:RKKeyPathByDeletingLastComponent(keyPath)
+ entityMapping:(RKEntityMapping *)propertyMapping.objectMapping]];
+ }
+ if ([propertyMapping isKindOfClass:[RKRelationshipMapping class]]) {
+ if ([[(RKRelationshipMapping *)propertyMapping mapping] isKindOfClass:[RKEntityMapping class]]) {
+ [entityMappingEvents addObject:[RKEntityMappingEvent eventWithRootKey:rootKey keyPath:keyPath entityMapping:(RKEntityMapping *)[(RKRelationshipMapping *)propertyMapping mapping]]];
+ }
+ }
+ }];
+ }];
+ self.entityMappingEvents = entityMappingEvents;
+}
+
@end
View
34 Code/Network/RKObjectManager.h
@@ -202,6 +202,15 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
Please see the documentation for `RKRouter`, `RKRouteSet`, and `RKRoute` for more details about the routing classes.
+ ## Metadata Mapping
+
+ The `RKObjectManager` class has integrated support for metadata mapping. Metdata mapping enables the object mapping of supplemental information external to the object representation loaded via an HTTP response. Object request operations constructed by the manager make the following metadata key paths available for mapping:
+
+ 1. `@metadata.routing.parameters` - A dictionary whose keys are the key paths matched from the path pattern of the `RKRoute` object used to construct the request URL and whose values are taken by evaluating the key path against the object interpolated with the route. Only available when routing was used to construct the request URL.
+ 1. `@metadata.routing.route` - The route object used to construct the request URL.
+
+ Please refer to the documentation accompanying `RKMappingOperation` for more details on metadata mapping.
+
## Core Data
RestKit features deep integration with Apple's Core Data persistence framework. The object manager provides access to this integration by creating `RKManagedObjectRequestOperation` objects when an attempt is made to interact with a resource that has been mapped using an `RKEntityMapping`. To utilize the Core Data integration, the object manager must be provided with a fully configured `RKManagedObjectStore` object. The `RKManagedObjectStore` provides access to the `NSManagedObjectModel` and `NSManagedObjectContext` objects required to peform object mapping that targets a Core Data entity.
@@ -421,20 +430,29 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
///-----------------------------------------
/**
- Sets the `RKHTTPRequestOperation` subclass to be used when constructing HTTP request operations for requests dispatched through the manager.
+ Attempts to register a subclass of `RKHTTPRequestOperation` or `RKObjectRequestOperation`, adding it to a list of classes that are consulted each time the receiver needs to construct an HTTP or object request operation with a URL request.
+
+ When `objectRequestOperationWithRequest:success:failure:` or `managedObjectRequestOperationWithRequest:managedObjectContext:success:failure:` is invoked, each registered subclass is consulted to see if it can handle the request. The first class to return `YES` when sent a `+ canProcessRequest:` message is used to create an operation using `initWithHTTPRequestOperation:responseDescriptors:`. The type of HTTP request operation used to initialize the object request operation is determined by evaluating the subclasses of `RKHTTPRequestOperation` registered via `registerRequestOperationClass:` and defaults to `RKHTTPRequestOperation`.
- When set, an instance of the given class will be initialized via `initWithRequest:` each time that the receiver constructs an HTTP request operation. HTTP request operations are used to initialize instances of `RKObjectRequestOperation` and are responsible for managing the HTTP request/response lifecycle of a request whose response is destined to be object mapped. Providing a subclass implementation of `RKHTTPRequestOperation` allows the behavior of all requests sent through the manager to be changed.
+ There is no guarantee that all registered classes will be consulted. The object manager will only consider direct subclasses of `RKObjectRequestOperation` when `objectRequestOperationWithRequest:success:failure` is called and will only consider subclasses of `RKManagedObjectRequestOperation` when `managedObjectRequestOperationWithRequest:managedObjectContext:success:failure:` is called. If you wish to map a mixture of managed and unmanaged objects within the same object request operation you must register a `RKManagedObjectRequestOperation` subclass. Classes are consulted in the reverse order of their registration. Attempting to register an already-registered class will move it to the top of the list.
- @param operationClass A class object inheriting from `RKHTTPRequestOperation` to be used for HTTP requests dispatched through the manager.
- @raises `NSInvalidArgumentException` Raised if the given class does not inherit from `RKHTTPRequestOperation`.
- @see `RKHTTPRequestOperation`
- @warning The given class must inherit from `RKHTTPRequestOperation`, else an exception will be raised.
+ @param operationClass The subclass of `RKHTTPRequestOperation` or `RKObjectRequestOperation` to register.
+ @return `YES` if the given class was registered successfully, else `NO`. The only failure condition is if `operationClass` is not a subclass of `RKHTTPRequestOperation` or `RKObjectRequestOperation`.
*/
-- (void)setHTTPOperationClass:(Class)operationClass;
+- (BOOL)registerRequestOperationClass:(Class)operationClass;
+
+/**
+ Unregisters the specified subclass of `RKHTTPRequestOperation` or `RKObjectRequestOperation` from the list of classes consulted when `objectRequestOperationWithRequest:success:failure:` or `managedObjectRequestOperationWithRequest:managedObjectContext:success:failure:` is called.
+
+ @param operationClass The subclass of `RKHTTPRequestOperation` or `RKObjectRequestOperation` to unregister.
+ */
+- (void)unregisterRequestOperationClass:(Class)operationClass;
/**
Creates an `RKObjectRequestOperation` operation with the given request and sets the completion block with the given success and failure blocks.
+ In order to determine what kind of operation is created, each registered `RKObjectRequestOperation` subclass is consulted (in reverse order of when they were specified) to see if it can handle the specific request. The first class to return `YES` when sent a `canProcessRequest:` message is used to create an operation using `initWithHTTPRequestOperation:responseDescriptors:`. The type of HTTP request operation used to initialize the object request operation is determined by evaluating the subclasses of `RKHTTPRequestOperation` registered via `registerRequestOperationClass:` and defaults to `RKHTTPRequestOperation`.
+
@param request The request object to be loaded asynchronously during execution of the operation.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request.
@param failure A block object to be executed when the request operation finishes unsuccessfully, or that finishes successfully, but encountered an error while parsing the resonse data. This block has no return value and takes two arguments:, the created request operation and the `NSError` object describing the network or parsing error that occurred.
@@ -451,6 +469,8 @@ RKMappingResult, RKRequestDescriptor, RKResponseDescriptor;
The given managed object context given will be used as the parent context of the private managed context in which the response is mapped and will be used to fetch the results upon invocation of the success completion block.
+ In order to determine what kind of operation is created, each registered `RKManagedObjectRequestOperation` subclass is consulted (in reverse order of when they were specified) to see if it can handle the specific request. The first class to return `YES` when sent a `canProcessRequest:` message is used to create an operation using `initWithHTTPRequestOperation:responseDescriptors:`. The type of HTTP request operation used to initialize the object request operation is determined by evaluating the subclasses of `RKHTTPRequestOperation` registered via `registerRequestOperationClass:` and defaults to `RKHTTPRequestOperation`.
+
@param request The request object to be loaded asynchronously during execution of the operation.
@param managedObjectContext The managed object context with which to associate the operation. This context will be used as the parent context of a new operation local `NSManagedObjectContext` with the `NSPrivateQueueConcurrencyType` concurrency type. Upon success, the private context will be saved and changes resulting from the object mapping will be 'pushed' to the given context.
@param success A block object to be executed when the request operation finishes successfully. This block has no return value and takes two arguments: the created object request operation and the `RKMappingResult` object created by object mapping the response data of request.
View
141 Code/Network/RKObjectManager.m
@@ -277,7 +277,10 @@ @interface RKObjectManager ()
@property (nonatomic, strong) NSMutableArray *mutableRequestDescriptors;
@property (nonatomic, strong) NSMutableArray *mutableResponseDescriptors;
@property (nonatomic, strong) NSMutableArray *mutableFetchRequestBlocks;
-@property (nonatomic) Class HTTPOperationClass;
+@property (nonatomic, strong) NSMutableArray *registeredHTTPRequestOperationClasses;
+@property (nonatomic, strong) NSMutableArray *registeredObjectRequestOperationClasses;
+@property (nonatomic, strong) NSMutableArray *registeredManagedObjectRequestOperationClasses;
+
@end
@implementation RKObjectManager
@@ -292,6 +295,9 @@ - (id)initWithHTTPClient:(AFHTTPClient *)client
self.mutableRequestDescriptors = [NSMutableArray new];
self.mutableResponseDescriptors = [NSMutableArray new];
self.mutableFetchRequestBlocks = [NSMutableArray new];
+ self.registeredHTTPRequestOperationClasses = [NSMutableArray new];
+ self.registeredManagedObjectRequestOperationClasses = [NSMutableArray new];
+ self.registeredObjectRequestOperationClasses = [NSMutableArray new];
self.requestSerializationMIMEType = RKMIMETypeFromAFHTTPClientParameterEncoding(client.parameterEncoding);
// Set shared manager if nil
@@ -437,16 +443,43 @@ - (NSMutableURLRequest *)multipartFormRequestWithObject:(id)object
return multipartRequest;
}
-- (void)setHTTPOperationClass:(Class)operationClass
+#pragma mark - Registering Subclasses
+
+- (BOOL)registerRequestOperationClass:(Class)operationClass
+{
+ if ([operationClass isSubclassOfClass:[RKManagedObjectRequestOperation class]]) {
+ [self.registeredManagedObjectRequestOperationClasses removeObject:operationClass];
+ [self.registeredManagedObjectRequestOperationClasses insertObject:operationClass atIndex:0];
+ return YES;
+ } else if ([operationClass isSubclassOfClass:[RKObjectRequestOperation class]]) {
+ [self.registeredObjectRequestOperationClasses removeObject:operationClass];
+ [self.registeredObjectRequestOperationClasses insertObject:operationClass atIndex:0];
+ return YES;
+ } else if ([operationClass isSubclassOfClass:[RKHTTPRequestOperation class]]) {
+ [self.registeredHTTPRequestOperationClasses removeObject:operationClass];
+ [self.registeredHTTPRequestOperationClasses insertObject:operationClass atIndex:0];
+ return YES;
+ }
+
+ return NO;
+}
+
+- (void)unregisterRequestOperationClass:(Class)operationClass
{
- NSAssert(operationClass == nil || [operationClass isSubclassOfClass:[RKHTTPRequestOperation class]], @"The HTTP operation class must be a subclass of `RKHTTPRequestOperation`");
- _HTTPOperationClass = operationClass;
+ [self.registeredHTTPRequestOperationClasses removeObject:operationClass];
+ [self.registeredObjectRequestOperationClasses removeObject:operationClass];
+ [self.registeredManagedObjectRequestOperationClasses removeObject:operationClass];
}
-- (RKHTTPRequestOperation *)HTTPOperationWithRequest:(NSURLRequest *)request
+- (Class)requestOperationClassForRequest:(NSURLRequest *)request fromRegisteredClasses:(NSArray *)registeredClasses
{
- Class operationClass = self.HTTPOperationClass ?: [RKHTTPRequestOperation class];
- return [[operationClass alloc] initWithRequest:request];
+ Class requestOperationClass = nil;
+ NSEnumerator *enumerator = [registeredClasses reverseObjectEnumerator];
+ while (requestOperationClass = [enumerator nextObject]) {
+ if ([requestOperationClass canProcessRequest:request]) break;
+ requestOperationClass = nil;
+ }
+ return requestOperationClass;
}
#pragma mark - Object Request Operations
@@ -455,7 +488,10 @@ - (RKObjectRequestOperation *)objectRequestOperationWithRequest:(NSURLRequest *)
success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success
failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure
{
- RKObjectRequestOperation *operation = [[RKObjectRequestOperation alloc] initWithHTTPRequestOperation:[self HTTPOperationWithRequest:request] responseDescriptors:self.responseDescriptors];
+ Class HTTPRequestOperationClass = [self requestOperationClassForRequest:request fromRegisteredClasses:self.registeredHTTPRequestOperationClasses] ?: [RKHTTPRequestOperation class];
+ RKHTTPRequestOperation *HTTPRequestOperation = [[HTTPRequestOperationClass alloc] initWithRequest:request];
+ Class objectRequestOperationClass = [self requestOperationClassForRequest:request fromRegisteredClasses:self.registeredObjectRequestOperationClasses] ?: [RKObjectRequestOperation class];
+ RKObjectRequestOperation *operation = [[objectRequestOperationClass alloc] initWithHTTPRequestOperation:HTTPRequestOperation responseDescriptors:self.responseDescriptors];
[operation setCompletionBlockWithSuccess:success failure:failure];
return operation;
}
@@ -465,7 +501,10 @@ - (RKManagedObjectRequestOperation *)managedObjectRequestOperationWithRequest:(N
success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success
failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure
{
- RKManagedObjectRequestOperation *operation = [[RKManagedObjectRequestOperation alloc] initWithHTTPRequestOperation:[self HTTPOperationWithRequest:request] responseDescriptors:self.responseDescriptors];
+ Class HTTPRequestOperationClass = [self requestOperationClassForRequest:request fromRegisteredClasses:self.registeredHTTPRequestOperationClasses] ?: [RKHTTPRequestOperation class];
+ RKHTTPRequestOperation *HTTPRequestOperation = [[HTTPRequestOperationClass alloc] initWithRequest:request];
+ Class objectRequestOperationClass = [self requestOperationClassForRequest:request fromRegisteredClasses:self.registeredManagedObjectRequestOperationClasses] ?: [RKManagedObjectRequestOperation class];
+ RKManagedObjectRequestOperation *operation = (RKManagedObjectRequestOperation *)[[objectRequestOperationClass alloc] initWithHTTPRequestOperation:HTTPRequestOperation responseDescriptors:self.responseDescriptors];
[operation setCompletionBlockWithSuccess:success failure:failure];
operation.managedObjectContext = managedObjectContext ?: self.managedObjectStore.mainQueueManagedObjectContext;
operation.managedObjectCache = self.managedObjectStore.managedObjectCache;
@@ -480,8 +519,16 @@ - (id)appropriateObjectRequestOperationWithObject:(id)object
{
RKObjectRequestOperation *operation = nil;
NSURLRequest *request = [self requestWithObject:object method:method path:path parameters:parameters];
- NSString *requestPath = (path) ? path : [[self.router URLForObject:object method:method] relativeString];
- NSArray *matchingDescriptors = RKFilteredArrayOfResponseDescriptorsMatchingPath(self.responseDescriptors, requestPath);
+ NSDictionary *routingMetadata = nil;
+ if (! path) {
+ RKRoute *route = [self.router.routeSet routeForObject:object method:method];
+ NSDictionary *interpolatedParameters = nil;
+ NSURL *URL = [self URLWithRoute:route object:object interpolatedParameters:&interpolatedParameters];
+ path = [URL relativeString];
+ routingMetadata = @{ @"routing": @{ @"parameters": interpolatedParameters, @"route": route } };
+ }
+
+ NSArray *matchingDescriptors = RKFilteredArrayOfResponseDescriptorsMatchingPath(self.responseDescriptors, path);
BOOL containsEntityMapping = RKDoesArrayOfResponseDescriptorsContainEntityMapping(matchingDescriptors);
BOOL isManagedObjectRequestOperation = (containsEntityMapping || [object isKindOfClass:[NSManagedObject class]]);
@@ -490,15 +537,21 @@ - (id)appropriateObjectRequestOperationWithObject:(id)object
// Construct a Core Data operation
NSManagedObjectContext *managedObjectContext = [object respondsToSelector:@selector(managedObjectContext)] ? [object managedObjectContext] : self.managedObjectStore.mainQueueManagedObjectContext;
operation = [self managedObjectRequestOperationWithRequest:request managedObjectContext:managedObjectContext success:nil failure:nil];
- if ([object isKindOfClass:[NSManagedObject class]] && [[object objectID] isTemporaryID]) {
- RKLogInfo(@"Asked to perform object request with NSManagedObject with temporary object ID: Obtaining permanent ID before proceeding.");
- __block BOOL _blockSuccess;
- __block NSError *_blockError;
-
- [[object managedObjectContext] performBlockAndWait:^{
- _blockSuccess = [[object managedObjectContext] obtainPermanentIDsForObjects:@[object] error:&_blockError];
- }];
- if (! _blockSuccess) RKLogWarning(@"Failed to obtain permanent ID for object %@: %@", object, _blockError);
+
+ if ([object isKindOfClass:[NSManagedObject class]]) {
+ static NSPredicate *temporaryObjectsPredicate = nil;
+ if (! temporaryObjectsPredicate) temporaryObjectsPredicate = [NSPredicate predicateWithFormat:@"objectID.isTemporaryID == YES"];
+ NSSet *temporaryObjects = [[managedObjectContext insertedObjects] filteredSetUsingPredicate:temporaryObjectsPredicate];
+ if ([temporaryObjects count]) {
+ RKLogInfo(@"Asked to perform object request for NSManagedObject with temporary object IDs: Obtaining permanent ID before proceeding.");
+ __block BOOL _blockSuccess;
+ __block NSError *_blockError;
+
+ [[object managedObjectContext] performBlockAndWait:^{
+ _blockSuccess = [[object managedObjectContext] obtainPermanentIDsForObjects:[temporaryObjects allObjects] error:&_blockError];
+ }];
+ if (! _blockSuccess) RKLogWarning(@"Failed to obtain permanent ID for object %@: %@", object, _blockError);
+ }
}
} else {
// Non-Core Data operation
@@ -506,18 +559,38 @@ - (id)appropriateObjectRequestOperationWithObject:(id)object
}
if (RKDoesArrayOfResponseDescriptorsContainMappingForClass(self.responseDescriptors, [object class])) operation.targetObject = object;
+ operation.mappingMetadata = routingMetadata;
return operation;
}
+- (NSURL *)URLWithRoute:(RKRoute *)route object:(id)object interpolatedParameters:(NSDictionary **)interpolatedParameters
+{
+ NSString *path = nil;
+ if (object) {
+ RKPathMatcher *pathMatcher = [RKPathMatcher pathMatcherWithPattern:route.pathPattern];
+ path = [pathMatcher pathFromObject:object addingEscapes:route.shouldEscapePath interpolatedParameters:interpolatedParameters];
+ } else {
+ // When there is no object, the path pattern is our complete path
+ path = route.pathPattern;
+ if (interpolatedParameters) *interpolatedParameters = @{};
+ }
+ return [NSURL URLWithString:path relativeToURL:self.baseURL];
+}
+
- (void)getObjectsAtPathForRelationship:(NSString *)relationshipName
ofObject:(id)object
parameters:(NSDictionary *)parameters
success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success
failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure
{
- NSURL *URL = [self.router URLForRelationship:relationshipName ofObject:object method:RKRequestMethodGET];
+ RKRoute *route = [self.router.routeSet routeForRelationship:relationshipName ofClass:[object class] method:RKRequestMethodGET];
+ NSDictionary *interpolatedParameters = nil;
+ NSURL *URL = [self URLWithRoute:route object:object interpolatedParameters:&interpolatedParameters];
NSAssert(URL, @"Failed to generate URL for relationship named '%@' for object: %@", relationshipName, object);
- return [self getObjectsAtPath:[URL relativeString] parameters:parameters success:success failure:failure];
+ RKObjectRequestOperation *operation = [self appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:[URL relativeString] parameters:parameters];
+ operation.mappingMetadata = @{ @"routing": @{ @"parameters": interpolatedParameters, @"route": route } };
+ [operation setCompletionBlockWithSuccess:success failure:failure];
+ [self enqueueObjectRequestOperation:operation];
}
- (void)getObjectsAtPathForRouteNamed:(NSString *)routeName
@@ -526,13 +599,18 @@ - (void)getObjectsAtPathForRouteNamed:(NSString *)routeName
success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success
failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure
{
- NSParameterAssert(routeName);
- RKRequestMethod method;
- NSURL *URL = [self.router URLForRouteNamed:routeName method:&method object:object];
+ NSParameterAssert(routeName);
+ RKRoute *route = [self.router.routeSet routeForName:routeName];
+ NSDictionary *interpolatedParameters = nil;
+ NSURL *URL = [self URLWithRoute:route object:object interpolatedParameters:&interpolatedParameters];
NSAssert(URL, @"No route found named '%@'", routeName);
- NSString *path = [URL relativeString];
- NSAssert(method == RKRequestMethodGET, @"Expected route named '%@' to specify a GET, but it does not", routeName);
- return [self getObjectsAtPath:path parameters:parameters success:success failure:failure];
+ NSAssert(route.method == RKRequestMethodGET, @"Expected route named '%@' to specify a GET, but it does not", routeName);
+
+ RKObjectRequestOperation *operation = [self appropriateObjectRequestOperationWithObject:nil method:RKRequestMethodGET path:[URL relativeString] parameters:parameters];
+ operation.mappingMetadata = @{ @"routing": @{ @"parameters": interpolatedParameters, @"route": route } };
+ [operation setCompletionBlockWithSuccess:success failure:failure];
+ [self enqueueObjectRequestOperation:operation];
+
}
- (void)getObjectsAtPath:(NSString *)path
@@ -615,7 +693,8 @@ - (RKPaginator *)paginatorWithPathPattern:(NSString *)pathPattern
paginator.managedObjectCache = self.managedObjectStore.managedObjectCache;
paginator.fetchRequestBlocks = self.fetchRequestBlocks;
paginator.operationQueue = self.operationQueue;
- if (self.HTTPOperationClass) [paginator setHTTPOperationClass:self.HTTPOperationClass];
+ Class HTTPOperationClass = [self requestOperationClassForRequest:request fromRegisteredClasses:self.registeredHTTPRequestOperationClasses];
+ if (HTTPOperationClass) [paginator setHTTPOperationClass:HTTPOperationClass];
return paginator;
}
@@ -723,13 +802,15 @@ - (void)enqueueBatchOfObjectRequestOperationsWithRoute:(RKRoute *)route
NSMutableArray *operations = [[NSMutableArray alloc] initWithCapacity:objects.count];
for (id object in objects) {
RKObjectRequestOperation *operation = nil;
- NSURL *URL = [self.router URLWithRoute:route object:object];
+ NSDictionary *interpolatedParameters = nil;
+ NSURL *URL = [self URLWithRoute:route object:object interpolatedParameters:&interpolatedParameters];
NSAssert(URL, @"Failed to generate URL for route %@ with object %@", route, object);
if ([route isClassRoute]) {
operation = [self appropriateObjectRequestOperationWithObject:object method:route.method path:[URL relativeString] parameters:nil];
} else {
operation = [self appropriateObjectRequestOperationWithObject:nil method:route.method path:[URL relativeString] parameters:nil];
}
+ operation.mappingMetadata = @{ @"routing": interpolatedParameters, @"route": route };
[operations addObject:operation];
}
return [self enqueueBatchOfObjectRequestOperations:operations progress:progress completion:completion];
View
2  Code/Network/RKObjectParameterization.m
@@ -126,7 +126,7 @@ - (void)mappingOperation:(RKMappingOperation *)operation didSetValue:(id)value f
if (transformedValue) {
RKLogDebug(@"Serialized %@ value at keyPath to %@ (%@)", NSStringFromClass([value class]), NSStringFromClass([transformedValue class]), value);
- [operation.destinationObject setValue:transformedValue forKey:keyPath];
+ [operation.destinationObject setValue:transformedValue forKeyPath:keyPath];
}
}
View
26 Code/Network/RKObjectRequestOperation.h
@@ -29,12 +29,16 @@
## Acceptable Content Types and Status Codes
- Instances of `RKObjectRequestOperation` determine the acceptability of status codes and content types differently than is typical for `AFNetworking` derived network opertations. The `RKHTTPRequestOperation` (which is a subclass of the AFNetworking `AFHTTPRequestOperation` class) supports the dynamic assigning of acceptable status codes and content types. This facility is utilized during the configuration of the network operation for an object request operation. The set of acceptable content types is determined by consulting the `RKMIMETypeSerialization` via an invocation of `[RKMIMETypeSerialization registeredMIMETypes]`. The `registeredMIMETypes` method returns an `NSSet` containing either `NSString` or `NSRegularExpression` objects that specify the content types for which `RKSerialization` classes have been registered to handle. The set of acceptable status codes is determined by aggregating the value of the `statusCodes` property from all registered `RKResponseDescriptor` objects.
+ Instances of `RKObjectRequestOperation` determine the acceptability of status codes and content types differently than is typical for `AFNetworking` derived network opertations. The `RKHTTPRequestOperation` (which is a subclass of the AFNetworking `AFHTTPRequestOperation` class) supports the dynamic assignment of acceptable status codes and content types. This facility is utilized during the configuration of the network operation for an object request operation. The set of acceptable content types is determined by consulting the `RKMIMETypeSerialization` via an invocation of `[RKMIMETypeSerialization registeredMIMETypes]`. The `registeredMIMETypes` method returns an `NSSet` containing either `NSString` or `NSRegularExpression` objects that specify the content types for which `RKSerialization` classes have been registered to handle. The set of acceptable status codes is determined by aggregating the value of the `statusCodes` property from all registered `RKResponseDescriptor` objects.
## Error Mapping
If the HTTP request returned a response in the Client Error (400-499 range) or Server Error (500-599 range) class and an appropriate `RKResponseDescriptor` is provided to perform mapping on the response, then the object mapping result is considered to contain a server returned error. In this case, an `NSError` object is created in the `RKErrorDomain` with an error code of `RKMappingErrorFromMappingResult` and the object request operation is failed. In the event that an a response is returned in an error class and no `RKResponseDescriptor` has been provided to the operation to handle it, then an `NSError` object in the `AFNetworkingErrorDomain` with an error code of `NSURLErrorBadServerResponse` will be returned by the underlying `RKHTTPRequestOperation` indicating that an unexpected status code was returned.
+ ## Metadata Mapping
+
+ The `RKObjectRequestOperation` class provides support for metadata mapping via the `mappingMetadata` property. This optional dictionary of user supplied information is made available to the mapping operations executed when processing the HTTP response loaded by an object request operation. More details about the metadata mapping architecture is available on the `RKMappingOperation` documentation.
+
## Prioritization and Cancellation
Object request operations support prioritization and cancellation of the underlying `RKHTTPRequestOperation` and `RKResponseMapperOperation` operations that perform the network transport and object mapping duties on their behalf. The queue priority of the object request operation, as set via the `[NSOperation setQueuePriority:]` method, is applied to the underlying response mapping operation when it is enqueued onto the `responseMappingQueue`. If the object request operation is cancelled, then the underlying HTTP request operation and response mapping operation are also cancelled.
@@ -95,6 +99,8 @@
/**
The array of `RKResponseDescriptor` objects that specify how the deserialized `responseData` is to be object mapped.
+
+ The response descriptors define the acceptable HTTP Status Codes of the receiver.
*/
@property (nonatomic, strong, readonly) NSArray *responseDescriptors;
@@ -105,6 +111,11 @@
*/
@property (nonatomic, strong) id targetObject;
+/**
+ An optional dictionary of metadata to make available to mapping operations executed while processing the HTTP response loaded by the receiver.
+ */
+@property (nonatomic, copy) NSDictionary *mappingMetadata;
+
///----------------------------------
/// @name Accessing Operation Results
///----------------------------------
@@ -112,7 +123,7 @@
/**
The mapping result returned by the underlying `RKObjectResponseMapperOperation`.
- This property is `nil` if the operation is failed due to a network transport error.
+ This property is `nil` if the operation is failed due to a network transport error or no mapping was peformed on the response.
*/
@property (nonatomic, strong, readonly) RKMappingResult *mappingResult;
@@ -168,6 +179,17 @@
*/
- (void)setWillMapDeserializedResponseBlock:(id (^)(id deserializedResponseBody))block;
+///-----------------------------------------------------
+/// @name Determining Whether a Request Can Be Processed
+///-----------------------------------------------------
+
+/**
+ Returns a Boolean value determining whether or not the class can process the specified request.
+
+ @param request The request that is determined to be supported or not supported for this class.
+ */
++ (BOOL)canProcessRequest:(NSURLRequest *)request;
+
///-------------------------------------------
/// @name Accessing the Response Mapping Queue
///-------------------------------------------
View
39 Code/Network/RKObjectRequestOperation.m
@@ -20,6 +20,7 @@
#import "RKObjectRequestOperation.h"
#import "RKResponseMapperOperation.h"
+#import "RKResponseDescriptor.h"
#import "RKMIMETypeSerialization.h"
#import "RKHTTPUtilities.h"
#import "RKLog.h"
@@ -57,15 +58,16 @@ static void RKDecrementNetworkAcitivityIndicator()
return [NSString stringWithFormat:@"%@ '%@'", request.HTTPMethod, [request.URL absoluteString]];
}
-static NSIndexSet *RKObjectRequestOperationAcceptableStatusCodes()
+static NSIndexSet *RKAcceptableStatusCodesFromResponseDescriptors(NSArray *responseDescriptors)
{
- static NSMutableIndexSet *statusCodes = nil;
- if (! statusCodes) {
- statusCodes = [NSMutableIndexSet indexSet];
- [statusCodes addIndexesInRange:RKStatusCodeRangeForClass(RKStatusCodeClassSuccessful)];
- [statusCodes addIndexesInRange:RKStatusCodeRangeForClass(RKStatusCodeClassClientError)];
- }
- return statusCodes;
+ // If there are no response descriptors or any descriptor matches any status code (expressed by `statusCodes` == `nil`) then we want to accept anything
+ if ([responseDescriptors count] == 0 || [[responseDescriptors valueForKey:@"statusCodes"] containsObject:[NSNull null]]) return nil;
+
+ NSMutableIndexSet *acceptableStatusCodes = [NSMutableIndexSet indexSet];
+ [responseDescriptors enumerateObjectsUsingBlock:^(RKResponseDescriptor *responseDescriptor, NSUInteger idx, BOOL *stop) {
+ [acceptableStatusCodes addIndexes:responseDescriptor.statusCodes];
+ }];
+ return acceptableStatusCodes;
}
static NSString *RKStringForStateOfObjectRequestOperation(RKObjectRequestOperation *operation)
@@ -117,12 +119,19 @@ + (NSOperationQueue *)responseMappingQueue
return responseMappingQueue;
}
++ (BOOL)canProcessRequest:(NSURLRequest *)request
+{
+ return YES;
+}
+
- (void)dealloc
{
#if !OS_OBJECT_USE_OBJC
- if(_failureCallbackQueue) dispatch_release(_failureCallbackQueue);
- if(_successCallbackQueue) dispatch_release(_successCallbackQueue);
+ if (_failureCallbackQueue) dispatch_release(_failureCallbackQueue);
+ if (_successCallbackQueue) dispatch_release(_successCallbackQueue);
#endif
+ _failureCallbackQueue = NULL;
+ _successCallbackQueue = NULL;
}
// Designated initializer
@@ -136,7 +145,7 @@ - (id)initWithHTTPRequestOperation:(RKHTTPRequestOperation *)requestOperation re
self.responseDescriptors = responseDescriptors;
self.HTTPRequestOperation = requestOperation;
self.HTTPRequestOperation.acceptableContentTypes = [RKMIMETypeSerialization registeredMIMETypes];
- self.HTTPRequestOperation.acceptableStatusCodes = RKObjectRequestOperationAcceptableStatusCodes();
+ self.HTTPRequestOperation.acceptableStatusCodes = RKAcceptableStatusCodesFromResponseDescriptors(responseDescriptors);
}
return self;
@@ -246,10 +255,12 @@ - (void)setCompletionBlockWithSuccess:(void (^)(RKObjectRequestOperation *operat
- (RKMappingResult *)performMappingOnResponse:(NSError **)