This repository has been archived by the owner on Jun 2, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
MCHasManyAssociation.j
476 lines (381 loc) · 12.9 KB
/
MCHasManyAssociation.j
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
@implementation MCHasManyAssociation : MCAssociation
{
CPSet _associatedObjects;
BOOL _isLoadingAssociatedObjects;
BOOL _didLoadAssociatedObjects;
id _saveDelegate;
SEL _saveSelector;
id _loadDelegate;
SEL _loadSelector;
int _unsavedAssociationCount;
CPArray _sortDescriptors @accessors(setter=setSortDescriptors:);
CPArray _objectsToDelete;
CPURL _customURL @accessors(setter=setCustomURL:);
_CPObservableArray _observableAssociatedObjectArray;
}
- (id)initWithName:(CPString)aName class:(Class)aClass parent:(MCResource)aParent
{
if(self = [super initWithName:aName class:aClass parent:aParent])
{
_sortDescriptors = [];
_associatedObjects = [CPSet new];
_observableAssociatedObjectArray = [_CPObservableArray new];
_objectsToDelete = [];
_didLoadAssociatedObjects = NO;
_isLoadingAssociatedObjects = NO;
}
return self;
}
- (CPString)description
{
return [CPString stringWithFormat:@"<%@ 0x%@: \"%@\" on %@ (%d objects)>", class_getName(isa), [CPString stringWithHash:[self UID]], _associationName, [_parent className], [self count]];
}
#pragma mark -
#pragma mark Content and Array methods
- (CPSet)associatedObjectSet
{
return _associatedObjects;
}
- (_CPObservableArray)associatedObjects
{
[self loadAssociatedObjectsIfNeccessary];
return _observableAssociatedObjectArray;
}
- (void)addAssociatedObjects:(CPArray)theObjects
{
var objectEnumerator = [theObjects objectEnumerator],
object;
[_parent willChangeValueForKey:_associationName];
while(object = [objectEnumerator nextObject])
{
[self addAssociatedObjectInBatch:object];
}
// Re-sort if applicable
if(_sortDescriptors.length > 0)
{
[_observableAssociatedObjectArray sortUsingDescriptors:_sortDescriptors];
}
[_parent didChangeValueForKey:_associationName];
}
// By adding an object to this association, the following things happen:
// - reflection is set
- (void)addAssociatedObjectInBatch:(id)anObject
{
[anObject _setValue:self forKey:@"_reflection"];
// Clear Resource's URL, will be rebuilt
[anObject setResourceURL:nil];
var countBeforeInsertion = [_associatedObjects count];
[_associatedObjects addObject:anObject];
if([_associatedObjects count] > countBeforeInsertion)
{
[_observableAssociatedObjectArray addObject:anObject];
}
}
- (void)addAssociatedObject:(id)anObject
{
[_parent willChangeValueForKey:_associationName];
[self addAssociatedObjectInBatch:anObject];
// Re-sort if applicable
if(_sortDescriptors.length > 0)
{
[_observableAssociatedObjectArray sortUsingDescriptors:_sortDescriptors];
}
[_parent didChangeValueForKey:_associationName];
}
- (void)removeAssociatedObject:(id)anObject
{
[_parent willChangeValueForKey:_associationName];
[_objectsToDelete addObject:anObject];
[_associatedObjects removeObject:anObject];
[_observableAssociatedObjectArray removeObject:anObject];
[_parent didChangeValueForKey:_associationName];
}
- (void)removeAssociatedObjects:(CPArray)objects
{
[_parent willChangeValueForKey:_associationName];
[_objectsToDelete addObjectsFromArray:objects];
[_associatedObjects removeObjectsInArray:objects];
[_observableAssociatedObjectArray removeObjectsInArray:objects];
[_parent didChangeValueForKey:_associationName];
}
- (id)objectAtIndex:(int)index
{
[self loadAssociatedObjectsIfNeccessary];
if([_observableAssociatedObjectArray count] >= index)
{
return [_observableAssociatedObjectArray objectAtIndex:index];
}
else
{
return nil;
}
}
- (void)loadAssociatedObjectsWithDelegate:(id)aDelegate andSelector:(SEL)aSelector
{
_loadDelegate = aDelegate;
_loadSelector = aSelector;
[self loadAssociatedObjects];
}
- (void)loadAssociatedObjects
{
if(_nestedOnly)
return;
var loadRequest = [self _buildLoadRequest];
_isLoadingAssociatedObjects = YES;
[[MCQueue sharedQueue] appendRequest:loadRequest];
}
- (void)loadAssociatedObjectsIfNeccessary
{
// Cannot load associations on unsaved objects
if(![_parent identifier])
return;
if([_associatedObjects count] == 0 && !_didLoadAssociatedObjects && !_isLoadingAssociatedObjects)
{
[self loadAssociatedObjects];
}
}
- (BOOL)didLoad
{
return _didLoadAssociatedObjects;
}
- (BOOL)isLoading
{
return _isLoadingAssociatedObjects;
}
#pragma mark -
#pragma mark Actions
- (id)build
{
var newObject = [_associatedObjectClass new];
return newObject;
}
- (CPArray)saveWithDelegate:(id)aDelegate andSelector:(SEL)aSelector
{
return [self saveWithDelegate:aDelegate andSelector:aSelector startImmediately:YES];
}
- (CPArray)saveWithDelegate:(id)aDelegate andSelector:(SEL)aSelector startImmediately:(BOOL)startImmediately
{
var saveRequests = [self _buildSaveRequests];
_saveDelegate = aDelegate;
_saveSelector = aSelector;
if([saveRequests count] > 0 && startImmediately)
{
var associationSaveQueue = [MCQueue new];
[associationSaveQueue appendRequests:saveRequests];
[associationSaveQueue start];
}
else if(startImmediately && aDelegate && aSelector)
{
[_saveDelegate performSelector:_saveSelector withObject:self];
}
return saveRequests;
}
- (CPArray)removedObjects
{
return [_objectsToDelete copy];
}
// This method will actually delete the object on the server
- (void)deleteAssociatedObject:(id)anObject
{
if([_associatedObjects containsObject:anObject])
{
if([anObject isNewRecord])
{
[self didDeleteAssociatedObject:anObject]
}
else
{
[anObject deleteWithDelegate:self andSelector:@selector(didDeleteAssociatedObject:)];
}
}
else
{
[CPException raise:CPInvalidArgumentException reason:@"" + [anObject description] + " is not part of " + [self description]];
}
}
- (BOOL)hasErrors
{
var i = 0;
for(; i < [_associatedObjects count]; i++)
{
var associatedObject = [[_associatedObjects allObjects] objectAtIndex:i];
if([associatedObject hasErrors])
return YES;
}
return NO;
}
- (CPString)humanReadableErrors
{
var allAssociatedObjectErrors = [_associatedObjects valueForKeyPath:@"humanReadableErrors"];
if([allAssociatedObjectErrors isKindOfClass:[CPSet class]])
{
allAssociatedObjectErrors = [allAssociatedObjectErrors allObjects];
}
if(allAssociatedObjectErrors && ([allAssociatedObjectErrors isKindOfClass:[CPArray class]]))
return allAssociatedObjectErrors.join("\n");
else
return @"";
}
#pragma mark -
#pragma mark Helpers
- (MCQueuedRequest)_buildLoadRequest
{
var target = _customURL;
if(!target)
target = [CPURL URLWithString:[_parent resourceURL] + '/' + [_associatedObjectClass _constructResourceURL]];
var request = [MCHTTPRequest requestTarget:target withMethod:@"GET" andDelegate:self],
queuedRequest = [MCQueuedRequest queuedRequestWithRequest:request];
_isLoadingAssociatedObjects = YES;
return queuedRequest;
}
- (CPArray)_buildSaveRequests
{
var _unsavedAssociationCount = 0,
saveRequests = [];
// Add delete requests first
var deletedObjectEnumerator = [_objectsToDelete objectEnumerator],
deletedObject;
while(deletedObject = [deletedObjectEnumerator nextObject])
{
if(![deletedObject isNewRecord])
{
[saveRequests addObject:[deletedObject deleteWithDelegate:self andSelector:@selector(associatedObjectDidSave:) startImmediately:NO]];
_unsavedAssociationCount++;
}
}
[_objectsToDelete removeAllObjects];
var associatedObjectEnumerator = [_associatedObjects objectEnumerator],
associatedObject;
var uniqueAttributes = [_associatedObjectClass uniqueAttributes];
var requeuedSaveRequests = [];
while(associatedObject = [associatedObjectEnumerator nextObject])
{
[associatedObject commit];
var attributesForSave = [associatedObject attributesForSave];
if(attributesForSave || [associatedObject isNewRecord])
{
// Check for unique attributes
var uniqueAttributesToSave = [[[associatedObject changes] allKeys] objectsCommonWithArray:uniqueAttributes];
if(uniqueAttributesToSave && ![associatedObject isNewRecord])
{
var uniqueAttributesToSaveCount = [uniqueAttributesToSave count];
var associatedObjectClone = [associatedObject clone];
// Replace them with a temporary value before issuing the first save request
while(uniqueAttributesToSaveCount--)
{
var uniqueAttributeName = [uniqueAttributesToSave objectAtIndex:uniqueAttributesToSaveCount];
[associatedObjectClone setValue:MCGenerateShortRandom() forKey:uniqueAttributeName];
}
// Add the save request containing temporary values to the save queue
[saveRequests addObject:[associatedObjectClone saveWithDelegate:nil andSelector:nil startImmediately:NO]];
[requeuedSaveRequests addObject:[associatedObject saveWithDelegate:self andSelector:@selector(associatedObjectDidSave:) startImmediately:NO]];
}
else
{
var saveRequest = [associatedObject saveWithDelegate:self andSelector:@selector(associatedObjectDidSave:) startImmediately:NO];
[saveRequests addObject:saveRequest];
}
_unsavedAssociationCount++;
}
}
// Add any out-of-queue requests now (such as saving unique values that were replaced by temps earlier) as child requests of the last request in the "replaced" save queue
[[saveRequests lastObject] addChildRequests:requeuedSaveRequests];
return saveRequests;
}
- (CPURL)_buildAssociationURL
{
return [self _buildAssociationURLWithObject:nil];
}
- (CPString)_buildAssociationURLPrefix
{
if(_shallow)
{
return @"";
}
else
{
return [_parent resourceURL] + "/";
}
}
- (CPURL)_buildAssociationURLWithObject:(id)anObject
{
var URL,
URLBuildingObject = (anObject ? anObject : _associatedObjectClass),
lastURLPart = [URLBuildingObject _constructResourceURL];
if(_shallow)
{
URL = MCResourceServerURLPrefix + "/" + lastURLPart;
}
else
{
URL = [_parent resourceURL] + '/' + lastURLPart;
}
return [CPURL URLWithString:URL];
}
#pragma mark -
#pragma mark Callbacks
- (void)didDeleteAssociatedObject:(id)anObject
{
[_parent willChangeValueForKey:_associationName];
[self removeAssociatedObject:anObject];
[_parent didChangeValueForKey:_associationName];
}
// Will be called when associated objects were loaded either via -loadAssociatedObjects or
// via MCResource-setAttributes: method in case of nested objects
- (void)didLoadAssociatedObjects:(CPArray)associatedObjects
{
_didLoadAssociatedObjects = YES;
_isLoadingAssociatedObjects = NO;
[_parent willChangeValueForKey:_associationName];
// Remove all objects (except unsaved ones)
[_associatedObjects enumerateObjectsUsingBlock:function(associatedObject) {
if(![associatedObject isNewRecord])
{
[_associatedObjects removeObject:associatedObject];
[_observableAssociatedObjectArray removeObject:associatedObject];
}
}];
[self addAssociatedObjects:associatedObjects];
[_parent didChangeValueForKey:_associationName];
if(_loadDelegate && _loadSelector)
{
[_loadDelegate performSelector:_loadSelector withObject:associatedObjects];
}
}
- (void)associatedObjectDidSave:(id)object
{
if((--_unsavedAssociationCount) === 0)
{
[_saveDelegate performSelector:_saveSelector withObject:self];
}
}
- (void)requestDidFinish:(MCQueuedRequest)aRequest
{
[_associatedObjectClass _parseObjectsFromArray:[[aRequest HTTPRequest] responseData] intoArray:[] withDelegate:self andSelector:@selector(didLoadAssociatedObjects:)];
}
- (void)requestDidFail:(MCQueuedRequest)aRequest
{
CPLog.error(@"%@ – request did fail: %@", self, aRequest);
}
#pragma mark -
#pragma mark Method proxying
// We forward all array methods like "-objectsAtIndexes:", "-count", etc. to our
// observable array, so that straight-forward bindings are possible, like:
//
// [someController bind:@"content" toObject:aResource withKeyPath:@"associationName" options:nil]
//
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(![self respondsToSelector:aSelector] && [_observableAssociatedObjectArray respondsToSelector:aSelector])
return _observableAssociatedObjectArray;
return nil;
}
- (CPMethodSignature)methodSignatureForSelector:(SEL)aSelector
{
return nil;
}
- (void)forwardInvocation:(CPInvocation)anInvocation
{
[anInvocation invokeWithTarget:_associatedObjects];
}
@end