ActiveResourceKit  v1.2 (498.0)
 All Classes Files Functions Variables Typedefs Enumerator Properties Macros Pages
ARIncrementalStore.m
Go to the documentation of this file.
1 // ActiveResourceKit ARIncrementalStore.m
2 //
3 // Copyright © 2012, Roy Ratcliffe, Pioneering Software, United Kingdom
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the “Software”), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11 //
12 // The above copyright notice and this permission notice shall be included in
13 // all copies or substantial portions of the Software.
14 //
15 // THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER
16 // EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO
18 // EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
19 // OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
20 // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 // DEALINGS IN THE SOFTWARE.
22 //
23 //------------------------------------------------------------------------------
24 
25 #import "ARIncrementalStore.h"
27 #import "ARResource.h"
28 #import "ARService.h"
29 #import "ARErrors.h"
33 
34 #import <ActiveSupportKit/ActiveSupportKit.h>
35 
36 @interface ARIncrementalStore()
37 
38 - (id)executeFetchRequest:(NSFetchRequest *)request withContext:(NSManagedObjectContext *)context error:(NSError **)outError;
39 
40 - (id)executeSaveRequest:(NSSaveChangesRequest *)request withContext:(NSManagedObjectContext *)context error:(NSError **)outError;
41 
42 @end
43 
44 @implementation ARIncrementalStore
45 
46 + (void)initialize
47 {
48  if (self == [ARIncrementalStore class])
49  {
50  [self registerStoreClass];
51  }
52 }
53 
54 + (NSString *)storeType
55 {
57 }
58 
60 {
62 }
63 
64 + (NSString *)storeTypeForClass:(Class)aClass
65 {
66  return NSStringFromClass(aClass);
67 }
68 
69 + (void)registerStoreTypeForClass:(Class)aClass
70 {
71  [NSPersistentStoreCoordinator registerStoreClass:aClass forStoreType:[self storeTypeForClass:aClass]];
72 }
73 
74 // designated initialiser
75 - (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)root configurationName:(NSString *)name URL:(NSURL *)URL options:(NSDictionary *)options
76 {
77  self = [super initWithPersistentStoreCoordinator:root configurationName:name URL:URL options:options];
78  if (self)
79  {
80  _resourcesByObjectID = [[NSCache alloc] init];
81  [_resourcesByObjectID setDelegate:self];
82  [self setEntityNamePrefix:[options objectForKey:ARIncrementalStoreElementNamePrefixKey]];
83  }
84  return self;
85 }
86 
87 - (ARService *)serviceForEntityName:(NSString *)entityName
88 {
89  // The entity name may not correspond to a class name. You can use
90  // managed-object entities even without deriving a sub-class. However, the
91  // entity name follows class naming conventions: capitalised camel-case.
92  ARService *service = [[ARService alloc] initWithSite:[self URL] elementName:[self elementNameForEntityName:entityName]];
93  [service setConnection:[[ARSynchronousLoadingURLConnection alloc] init]];
94  return service;
95 }
96 
98 {
99  [_resourcesByObjectID removeAllObjects];
100 }
101 
102 //------------------------------------------------------------------------------
103 #pragma mark Active Resource-to-Core Data Names
104 //------------------------------------------------------------------------------
105 
106 @synthesize entityNamePrefix = _entityNamePrefix;
107 
108 - (NSString *)elementNameForEntityName:(NSString *)entityName
109 {
110  if ([self entityNamePrefix])
111  {
112  // The following implementation assumes that all entity names handled by
113  // this incremental store have the given prefix. It removes the prefix
114  // without first ensuring that the entity name begins with a matching
115  // string; design by contract. If the assumption fails, your application
116  // will throw an exception or otherwise fail.
117  entityName = [entityName substringFromIndex:[[self entityNamePrefix] length]];
118  }
119  return [[ASInflector defaultInflector] underscore:entityName];
120 }
121 
122 - (NSString *)entityNameForElementName:(NSString *)elementName
123 {
124  NSString *entityName = [[ASInflector defaultInflector] camelize:elementName uppercaseFirstLetter:YES];
125  if ([self entityNamePrefix])
126  {
127  entityName = [[self entityNamePrefix] stringByAppendingString:entityName];
128  }
129  return entityName;
130 }
131 
132 - (NSString *)attributeNameForPropertyName:(NSString *)propertyName
133 {
134  return [[ASInflector defaultInflector] underscore:propertyName];
135 }
136 
137 - (NSString *)propertyNameForAttributeName:(NSString *)attributeName
138 {
139  return [[ASInflector defaultInflector] camelize:attributeName uppercaseFirstLetter:NO];
140 }
141 
142 //------------------------------------------------------------------------------
143 #pragma mark Incremental Store Method Overrides
144 //------------------------------------------------------------------------------
145 
146 // The following methods appear in Core Data's public interface for
147 // NSIncrementalStore. Implementations below override the abstract interface
148 // laid out by Core Data.
149 
150 /*!
151  * @brief Validates the store URL.
152  * @details Is the store URL usable? Does it exist? Can the store receive save
153  * requests? Are the schemas compatible?
154  */
155 - (BOOL)loadMetadata:(NSError **)outError
156 {
157  NSMutableDictionary *metadata = [NSMutableDictionary dictionary];
158  [metadata setObject:[ARIncrementalStore storeTypeForClass:[self class]] forKey:NSStoreTypeKey];
159 
160  // Assigning a Universally-Unique ID is essential. Without this the next
161  // invocation of -[setMetadata:] will recurse infinitely. Use the URL
162  // description as the UUID. Within the context of Core Data, that should
163  // provide a sufficiently unique identifier while giving the store's
164  // managed-object IDs a meaningful and readable prefix.
165  [metadata setObject:[[self URL] description] forKey:NSStoreUUIDKey];
166 
167  [self setMetadata:[metadata copy]];
168  return YES;
169 }
170 
171 - (id)executeRequest:(NSPersistentStoreRequest *)request withContext:(NSManagedObjectContext *)context error:(NSError **)outError
172 {
173  switch ([request requestType])
174  {
175  case NSFetchRequestType:
176  return [self executeFetchRequest:(NSFetchRequest *)request withContext:context error:outError];
177  case NSSaveRequestType:
178  return [self executeSaveRequest:(NSSaveChangesRequest *)request withContext:context error:outError];
179  }
180  return nil;
181 }
182 
183 /*!
184  * @result If the request is a fetch request whose result type is set to one of
185  * @c NSManagedObjectResultType, @c NSManagedObjectIDResultType, @c
186  * NSDictionaryResultType, returns an array containing all objects in the store
187  * matching the request. If the request is a fetch request whose result type is
188  * set to @c NSCountResultType, returns an array containing an @c NSNumber of
189  * all objects in the store matching the request.
190  *
191  * This method runs on iOS, for instance, when a fetched results controller
192  * performs a fetch in response to a table view controller determining the
193  * number of sections in the table view.
194  *
195  * @par Fetch Request State
196  * Executing a fetch request requires decoding the fetch request. Fetch requests
197  * include numerous additional parameters, including:
198  *
199  * - group-by properties
200  * - predicate
201  * - values to fetch
202  * - entity description
203  * - offset
204  * - sort descriptors
205  * - batch size
206  * - fetch limit
207  * - relationship key paths for pre-fetching
208  * - flags
209  *
210  * Requests can be complex. In Objective-C terms, you can acquire the full
211  * fetch-request state using:
212  * @code
213  * NSString *entityName = [request entityName];
214  * NSPredicate *predicate = [request predicate];
215  * NSArray *sortDescriptors = [request sortDescriptors];
216  * NSUInteger fetchLimit = [request fetchLimit];
217  * NSArray *affectedStores = [request affectedStores];
218  * NSFetchRequestResultType resultType = [request resultType];
219  * BOOL includesSubentities = [request includesSubentities];
220  * BOOL includesPropertyValues = [request includesPropertyValues];
221  * BOOL returnsObjectsAsFaults = [request returnsObjectsAsFaults];
222  * NSArray *relationshipKeyPathsForPrefetching = [request relationshipKeyPathsForPrefetching];
223  * BOOL includesPendingChanges = [request includesPendingChanges];
224  * BOOL returnsDistinctResults = [request returnsDistinctResults];
225  * NSArray *propertiesToFetch = [request propertiesToFetch];
226  * NSUInteger fetchOffset = [request fetchOffset];
227  * NSUInteger fetchBatchSize = [request fetchBatchSize];
228  * BOOL shouldRefreshRefetchedObjects = [request shouldRefreshRefetchedObjects];
229  * NSArray *propertiesToGroupBy = [request propertiesToGroupBy];
230  * NSPredicate *havingPredicate = [request havingPredicate];
231  * @endcode
232  */
233 - (id)executeFetchRequest:(NSFetchRequest *)request withContext:(NSManagedObjectContext *)context error:(NSError **)outError
234 {
235  id __block result = nil;
236 
237  NSMutableDictionary *options = [NSMutableDictionary dictionary];
238  NSUInteger fetchLimit = [request fetchLimit];
239  if (fetchLimit)
240  {
241  [options setValue:[NSNumber numberWithUnsignedInteger:fetchLimit] forKey:@"limit"];
242  }
243  NSUInteger fetchOffset = [request fetchOffset];
244  if (fetchOffset)
245  {
246  [options setValue:[NSNumber numberWithUnsignedInteger:fetchOffset] forKey:@"offset"];
247  }
248  // Limit and offset only make sense for ordered resources. In fact, sort
249  // descriptors are mandatory when using fetched results controllers. With no
250  // sort descriptors, the controller throws an exception with reason, “An
251  // instance of NSFetchedResultsController requires a fetch request with sort
252  // descriptors.” The fetch request includes sort descriptors for this
253  // reason. Core Data may present multiple sort descriptors. Use plus symbols
254  // to separate sort keys followed by sort ascending or descending. Rails
255  // converts the plus to space on the server. This makes an important
256  // assumption about the server: that it responds correctly to the order
257  // parameter. Typically, the Rails index action does not handle client-side
258  // ordering requests. You can easily add the necessary ordering by adding
259  // order(params[:order]) to the Active Record find all directive, e.g. if
260  // your controller handles people then use the following statement to find
261  // and order all the people.
262  //
263  // @people = Person.order(params[:order]).all
264  //
265  // The order query reverts to default ordering when params[:order] is not
266  // present, equals nil.
267  //
268  // Use property-to-attribute name mapping to derive the sort key. The sort
269  // descriptor key exists in the Core Data namespace. However, the sort key
270  // for the ordering parameter must cross the HTTP connection and interact
271  // with the Rails namespace. The sort key follows property (Core Data)
272  // conventions at this side of the connection, but follows attribute (Active
273  // Resource) conventions at the far side of the connection.
274  NSMutableArray *orderStrings = [NSMutableArray array];
275  for (NSSortDescriptor *sortDescriptor in [request sortDescriptors])
276  {
277  [orderStrings addObject:[NSString stringWithFormat:@"%@+%@", [self attributeNameForPropertyName:[sortDescriptor key]], [sortDescriptor ascending] ? @"asc" : @"desc"]];
278  }
279  if ([orderStrings count])
280  {
281  [options setObject:[orderStrings componentsJoinedByString:@","] forKey:@"order"];
282  }
283  // Load up the resource cache first. Let new resources replace old// ones. The cache exists as a communication buffer between fetch// requests and other incremental-store interface methods. Do this// regardless of result type. It refreshes the cache with the latest// available resource attributes.// Compile the results for Core Data. Having previously iterated the// resources in order to create or update the cache, now deal with// the results in terms of managed objects and object IDs.// Relay the error. Note, this occurs here within a completion// block. Ensure that your original declaration for the error// pointer includes the __autoreleasing keyword, i.e.//// NSError *__autoreleasing error = nil;//// Otherwise, you could encounter EXC_BAD_ACCESS aborts.[[self serviceForEntityName:[request entityName]] findAllWithOptions:options completionHandler:^(ARHTTPResponse *response, NSArray *resources, NSError *error) {
284  if (resources)
285  {
286 
287 
288 
289 
290 
291  NSMutableArray *objectIDs = [NSMutableArray array];
292  for (ARResource *resource in resources)
293  {
294  [objectIDs addObject:[self objectIDForCachedResource:resource withContext:context]];
295  }
296 
297 
298 
299 
300  switch ([request resultType])
301  {
302  case NSManagedObjectResultType:
303  result = [NSMutableArray array];
304  for (NSManagedObjectID *objectID in objectIDs)
305  {
306  [(NSMutableArray *)result addObject:[context objectWithID:objectID]];
307  }
308  result = [result copy];
309  break;
310  case NSManagedObjectIDResultType:
311  result = [NSMutableArray array];
312  for (NSManagedObjectID *objectID in objectIDs)
313  {
314  [(NSMutableArray *)result addObject:objectID];
315  }
316  result = [result copy];
317  break;
318  case NSCountResultType:
319  result = [NSArray arrayWithObject:[NSNumber numberWithUnsignedInteger:[objectIDs count]]];
320  }
321  }
322  else
323  {
324 
325 
326 
327 
328 
329 
330 
331  if (outError && *outError == nil)
332  {
333  *outError = error;
334  }
335  }
336  }];
337 
338  return result;
339 }
340 
341 /*!
342  * @brief Core Data sends this message when managed-object contexts save.
343  * @details The save-changes request encapsulates inserted, updated and deleted
344  * objects.
345  */
346 - (id)executeSaveRequest:(NSSaveChangesRequest *)request withContext:(NSManagedObjectContext *)context error:(NSError **)outError
347 {
348  NSMutableArray *errors = [NSMutableArray array];
349 
350  // inserts
351  //
352  // Copy the insert-update-delete sets before iterating. This is necessary
353  // because the inner loop refreshes the object. Core Data adjusts the given
354  // sets when you refresh the object. Naughty, naughty. Apple documentation
355  // does not mention this subtlety. Copying the set effectively side-steps
356  // the collection mutation.
357  //
358  for (NSManagedObject *object in [[request insertedObjects] copy])
359  {
360  ARResource *resource = [self cachedResourceForObjectID:[object objectID] error:outError];
361  if (resource)
362  {
363  NSDictionary *foreignKeys = [self foreignKeysForObject:object resource:resource];
364  if ([foreignKeys count])
365  {
366  [resource mergeAttributes:foreignKeys];
367  [resource saveWithCompletionHandler:^(ARHTTPResponse *response, NSError *error) {
368  if (error == nil)
369  {
370  ;
371  }
372  else
373  {
374  [errors addObject:error];
375  }
376  }];
377  }
378  }
379 
380  // There is a good reason for refreshing an inserted object, even though
381  // at first sight reloading it appears odd. Are not the client and
382  // server synchronised after a resource insertion with respect to the
383  // inserted entities? No, because the server updates the created-at and
384  // the updated-at time stamps. Accessing the object again therefore
385  // requires a fetch in order to synchronise with the server. This
386  // principle also applies to updates, see below.
387  [self refreshObject:object];
388  }
389 
390  // updates
391  for (NSManagedObject *object in [[request updatedObjects] copy])
392  {
393  // Updates occur by rebuilding the Active Resource from its associated
394  // incremental node unless the resource already exists in the cache. You
395  // cannot assume that all the objects belong to the same entity
396  // description. Likely, the set will include different
397  // entities. Updating only occurs when saving the context. So updates
398  // include all modified entities in-between save events.
399  //
400  // Save the resources one-by-one. This implies one connection for each
401  // save operation. If there are many updates, could a bulk update
402  // optimise the number of connections? Ideally, there should be one
403  // create, one update and one delete request respectively containing all
404  // the objects to insert, update and delete.
405  NSEntityDescription *entity = [object entity];
406  ARResource *resource = [_resourcesByObjectID objectForKey:[object objectID]];
407  if (resource == nil)
408  {
409  resource = [[ARResource alloc] initWithService:[self serviceForEntityName:[entity name]]];
410  }
411  // Optimise PUT requests. Extract updated attributes from the managed
412  // object. Merge these with foreign-key updates. However, if the
413  // resulting merge exactly matches the cached resource attributes, then
414  // skip the resource save operation, as an optimisation. Do not compare
415  // attributes only appearing in the cached resource when finding
416  // differences; attributes only appearing in the resource cache have
417  // never been accessed or modified, likely because the client-side data
418  // model does not describe these ignored attributes. This makes an
419  // important assumption: that the resource always reflects its
420  // server-side state. The assumption remains true because the
421  // incremental store flushes the resource on insert, update and
422  // delete. Anything remaining in the cache was fetched from the server
423  // and therefore remains in-sync with the server until modified or
424  // deleted.
425  NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:[entity attributesFromObject:object]];
426  [attributes setValuesForKeysWithDictionary:[self foreignKeysForObject:object resource:resource]];
427  if ([attributes isEqualToDictionary:[[resource attributes] dictionaryWithValuesForKeys:[attributes allKeys]]])
428  {
429  continue;
430  }
431  [resource mergeAttributes:attributes];
432  [resource setPersisted:YES];
433  [resource saveWithCompletionHandler:^(ARHTTPResponse *response, NSError *error) {
434  if (error == nil)
435  {
436  [self refreshObject:object];
437  }
438  else
439  {
440  [errors addObject:error];
441  }
442  }];
443  }
444 
445  // deletes
446  for (NSManagedObject *object in [[request deletedObjects] copy])
447  {
448  NSNumber *ID = [self referenceObjectForObjectID:[object objectID]];
449  NSEntityDescription *entity = [object entity];
450  ARService *service = [self serviceForEntityName:[entity name]];
451  [service deleteWithID:ID options:nil completionHandler:^(ARHTTPResponse *response, NSError *error) {
452  if (error == nil)
453  {
454  [self refreshObject:object];
455  }
456  else
457  {
458  [errors addObject:error];
459  }
460  }];
461  }
462 
463  // results
464  BOOL success = [errors count] == 0;
465  if (!success && outError && *outError == nil)
466  {
467  *outError = [errors objectAtIndex:0];
468  }
469  return success ? [NSArray array] : nil;
470 }
471 
472 - (NSIncrementalStoreNode *)newValuesForObjectWithID:(NSManagedObjectID *)objectID withContext:(NSManagedObjectContext *)context error:(NSError **)outError
473 {
474  // If not already in the cache, turn the fault into a resource.
475  ARResource *resource = [self cachedResourceForObjectID:objectID error:outError];
476 
477  NSIncrementalStoreNode *node;
478  if (resource)
479  {
480  // Do not reflect null-valued properties in the snapshot. Take
481  // properties from the remote resource, find which if any have null
482  // values, then remove those key-value pairs. Core Data interprets
483  // missing attributes which appear in the data model as nils. Therefore
484  // missing equals nil and hence null equals nil, when
485  // missing. Otherwise, Core Data sends messages to the non-nil values,
486  // e.g. sends -length to string-type attributes; this raises an
487  // exception of course if the string equals [NSNull null] and not nil.
488  NSMutableDictionary *properties = [NSMutableDictionary dictionaryWithDictionary:[[objectID entity] propertiesFromResource:resource]];
489  [properties removeObjectsForKeys:[properties allKeysForObject:[NSNull null]]];
490  uint64_t version = [self versionForResource:resource];
491  node = [[NSIncrementalStoreNode alloc] initWithObjectID:objectID withValues:[properties copy] version:version];
492  }
493  else
494  {
495  node = nil;
496  if (outError && *outError == nil)
497  {
498  *outError = [NSError errorWithDomain:ARErrorDomain code:ARValuesForObjectError userInfo:nil];
499  }
500  }
501  return node;
502 }
503 
504 - (id)newValueForRelationship:(NSRelationshipDescription *)relationship forObjectWithID:(NSManagedObjectID *)objectID withContext:(NSManagedObjectContext *)context error:(NSError **)outError
505 {
506  id __block result;
507  // to-one
508  if ([relationship maxCount] == 1)
509  {
510  // Active resources implement to-one associations using foreign keys,
511  // meaning the incremental store expects the remote server to provide a
512  // "relationship_id" attribute for each to-one resource at the origin
513  // of the to-one association.
514  //
515  // Back references are a special case. Their identifiers do not exist
516  // within the resource attributes. Active Resource Kit splits off the
517  // identifier from the attributes for nested resources. The
518  // back-referencing identifier lives instead within the prefix
519  // options. Hence, resolve the relationship by looking first in the
520  // attributes; but then by looking if necessary in the prefix options.
521  ARResource *resource = [_resourcesByObjectID objectForKey:objectID];
522  NSString *relationshipName = [self attributeNameForPropertyName:[relationship name]];
523  NSString *foreignKey = [[ASInflector defaultInflector] foreignKey:relationshipName separateClassNameAndIDWithUnderscore:YES];
524  id foreignID = ASNilForNull([[resource attributes] objectForKey:foreignKey]);
525  if (foreignID == nil)
526  {
527  foreignID = [[resource prefixOptions] objectForKey:foreignKey];
528  }
529  if (foreignID == nil)
530  {
531  result = [NSNull null];
532  }
533  else
534  {
535  result = [self newObjectIDForEntity:[relationship destinationEntity] referenceObject:foreignID];
536  }
537  }
538  // to-many
539  else if ([relationship isToMany])
540  {
541  // Answer an array of managed-object identifiers. Using standard RESTful
542  // approaches, no interface exists for querying just the resource
543  // identifiers. Instead, ask for the resources entirely, all attributes
544  // included. Rely on the resource cache to save the attributes for later
545  // when the object identifiers change from faults to realised objects.
546  //
547  // At present, the nested service cannot appear in the cache because the
548  // cache indexes by entity name; nor would it be correct to cache by the
549  // destination entity name, because the nested resource may not
550  // correspond to the non-nested resource by the same name. Caching would
551  // be possible in future if the cache indexed by site path.
552  //
553  // Derive the resource identifier from the object identifier. Hence, the
554  // actual resource does not need to exist in the cache. If it does not
555  // exist, there is no need to load it, an optimisation.
556  ARService *service = [self serviceForEntityName:[[objectID entity] name]];
557  ARService *nestedService = [[ARService alloc] initWithSite:[service siteWithPrefixParameter]];
558  [nestedService setElementName:[[ASInflector defaultInflector] singularize:[relationship name]]];
559  [nestedService setConnection:[[ARSynchronousLoadingURLConnection alloc] init]];
560  NSDictionary *options = [NSDictionary dictionaryWithObject:[self referenceObjectForObjectID:objectID] forKey:[service foreignKey]];
561  [nestedService findAllWithOptions:options completionHandler:^(ARHTTPResponse *response, NSArray *resources, NSError *error) {
562  if (resources)
563  {
564  NSMutableArray *objectIDs = [NSMutableArray array];
565  for (ARResource *resource in resources)
566  {
567  [objectIDs addObject:[self objectIDForCachedResource:resource withContext:context]];
568  }
569  result = [objectIDs copy];
570  }
571  else
572  {
573  result = nil;
574  if (outError && *outError == nil)
575  {
576  *outError = error;
577  }
578  }
579  }];
580  }
581  else
582  {
583  result = nil;
584  if (outError && *outError == nil)
585  {
586  *outError = [NSError errorWithDomain:ARErrorDomain code:ARValueForRelationshipError userInfo:nil];
587  }
588  }
589  return result;
590 }
591 
592 /*!
593  * @details Invoked just before sending a save-changes request. Objects sent
594  * here have only a temporary object ID. Objective: to assign permanent IDs to
595  * newly inserted objects. Answers a set of matching object IDs. The
596  * implementation assumes that the given object's have IDs always of nil. It
597  * sends Active Resource "create with attributes" requests for each object in
598  * order to obtain each object's permanent ID, as assigned by the resource
599  * server.
600  *
601  * There is a synchronisation issue here. The given objects do not yet exist at
602  * the server side; they have no permanent identifiers and hence Core Data asks
603  * for those identifiers by invoking this override. The objects need
604  * creating. Their permanent identifiers appear at the server side when the
605  * remote application creates the associated record.
606  */
607 - (NSArray *)obtainPermanentIDsForObjects:(NSArray *)objects error:(NSError **)outError
608 {
609  NSMutableArray *objectIDs = [NSMutableArray array];
610  NSMutableArray *errors = [NSMutableArray array];
611  for (NSManagedObject *object in objects)
612  {
613  NSEntityDescription *entity = [object entity];
614  ARService *service = [self serviceForEntityName:[entity name]];
615  // Balance the object IDs against the objects. Resource creation// failure adds null to the resulting array. This is just a// token to assert this interface's object-to-object-ID mapping// principle.[service createWithAttributes:[entity attributesFromObject:object] completionHandler:^(ARHTTPResponse *response, ARResource *resource, NSError *error) {
616  if (resource)
617  {
618  NSManagedObjectID *objectID = [self newObjectIDForEntity:entity referenceObject:[resource ID]];
619  [_resourcesByObjectID setObject:resource forKey:objectID];
620  [objectIDs addObject:objectID];
621  }
622  else
623  {
624 
625 
626 
627 
628  [objectIDs addObject:[NSNull null]];
629  [errors addObject:error];
630  }
631  }];
632  }
633  BOOL success = [errors count] == 0;
634  if (!success && outError && *outError == nil)
635  {
636  *outError = [errors objectAtIndex:0];
637  }
638  return success ? [objectIDs copy] : nil;
639 }
640 
641 - (void)cache:(NSCache *)cache willEvictObject:(id)obj
642 {
643 
644 }
645 
646 @end
647 
648 NSString *ARIncrementalStoreElementNamePrefixKey = @"ARIncrementalStoreElementNamePrefix";