Use user catalog displayname when displaying an account node
[pithos-macos] / pithos-macos / PithosAccountNode.m
1 //
2 //  PithosAccountNode.m
3 //  pithos-macos
4 //
5 // Copyright 2011-2012 GRNET S.A. All rights reserved.
6 //
7 // Redistribution and use in source and binary forms, with or
8 // without modification, are permitted provided that the following
9 // conditions are met:
10 // 
11 //   1. Redistributions of source code must retain the above
12 //      copyright notice, this list of conditions and the following
13 //      disclaimer.
14 // 
15 //   2. Redistributions in binary form must reproduce the above
16 //      copyright notice, this list of conditions and the following
17 //      disclaimer in the documentation and/or other materials
18 //      provided with the distribution.
19 // 
20 // THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21 // OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27 // USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28 // AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 // ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 // POSSIBILITY OF SUCH DAMAGE.
32 // 
33 // The views and conclusions contained in the software and
34 // documentation are those of the authors and should not be
35 // interpreted as representing official policies, either expressed
36 // or implied, of GRNET S.A.
37
38 #import "PithosAccountNode.h"
39 #import "PithosContainerNode.h"
40 #import "ASIPithos.h"
41 #import "ASIPithosAccountRequest.h"
42 #import "ASIPithosAccount.h"
43 #import "ASIPithosContainer.h"
44 #import "ASIDownloadCache.h"
45 #import "PithosAccount.h"
46 #import "PithosUtilities.h"
47 #import "PithosActivityFacility.h"
48
49 static NSImage *sharedIcon = nil;
50
51 @implementation PithosAccountNode
52 @synthesize pithos, pithosAccount, translatedGroups;
53
54 + (void)initialize {
55         if (self == [PithosAccountNode class])
56         sharedIcon = [[NSWorkspace sharedWorkspace] iconForFileType:NSFileTypeForHFSTypeCode(kUserIcon)];
57 }
58
59 #pragma mark -
60 #pragma mark Object Lifecycle
61
62 - (id)initWithPithos:(ASIPithos *)aPithos {
63     if ((self = [super init])) {
64         pithos = aPithos;
65     }
66     return self;
67 }
68
69 - (void)dealloc {
70     [accountRequest clearDelegatesAndCancel];
71     [refreshMetadataAccountRequest clearDelegatesAndCancel];
72     [applyMetadataAccountRequest clearDelegatesAndCancel];
73 }
74
75 #pragma mark -
76 #pragma mark Properties
77
78 - (void)setPithos:(ASIPithos *)aPithos {
79     if (aPithos && ![aPithos isEqualTo:pithos]) {
80         pithos = aPithos;
81         url = nil;
82         [accountRequest clearDelegatesAndCancel];
83         accountRequest = nil;
84         [refreshMetadataAccountRequest clearDelegatesAndCancel];
85         refreshMetadataAccountRequest = nil;
86         [applyMetadataAccountRequest clearDelegatesAndCancel];
87         applyMetadataAccountRequest = nil;
88         reset = YES;
89     }
90 }
91
92 - (void)setPithosAccount:(ASIPithosAccount *)aPithosAccount {
93     if (!aPithosAccount) {
94         pithosAccount = aPithosAccount;
95         self.translatedGroups = [NSMutableDictionary dictionary];
96     } else if (![aPithosAccount isEqualTo:pithosAccount]) {
97         pithosAccount = aPithosAccount;
98         if (pithosAccountManager) {
99             NSMutableArray *UUIDs = [NSMutableArray array];
100             for (NSString *groupName in pithosAccount.groups) {
101                 [UUIDs addObjectsFromArray:[pithosAccount.groups objectForKey:groupName]];
102             }
103             if (UUIDs.count) {
104                 [pithosAccountManager updateUserCatalogForForDisplaynames:nil UUIDs:UUIDs];
105             }
106             
107             NSMutableDictionary *newTranslatedGroups = [NSMutableDictionary dictionaryWithCapacity:pithosAccount.groups.count];
108             for (NSString *groupName in pithosAccount.groups) {
109                 NSMutableArray *groupUsers = [NSMutableArray array];
110                 for (NSString *UUID in [pithosAccount.groups objectForKey:groupName]) {
111                     [groupUsers addObject:[pithosAccountManager displaynameForUUID:UUID safe:YES]];
112                 }
113                 [newTranslatedGroups setObject:groupUsers forKey:groupName];
114             }
115             self.translatedGroups = newTranslatedGroups;
116         } else {
117             self.translatedGroups = [pithosAccount.groups copy];
118         }
119     }
120 }
121
122 - (NSString *)url {
123     if (url == nil)
124         url = [[NSString alloc] initWithFormat:@"%@%@", 
125                (sharingAccount ? [pithos storageURLWithAuthUser:sharingAccount] : pithos.storageURL), 
126                (shared ? @"?shared" : @"")];
127     return url;
128 }
129
130 - (NSArray *)children {
131     @synchronized(self) {
132         if (reset) {
133             [accountRequest clearDelegatesAndCancel];
134             accountRequest = nil;
135             [refreshMetadataAccountRequest clearDelegatesAndCancel];
136             refreshMetadataAccountRequest = nil;
137             [applyMetadataAccountRequest clearDelegatesAndCancel];
138             applyMetadataAccountRequest = nil;
139             children = nil;
140             newChildren = nil;
141             self.pithosAccount = nil;
142             freshness = PithosNodeStateRefreshNeeded;
143             forcedRefresh = YES;
144             reset = NO;
145             [self postChildrenUpdatedNotificationName];
146         }
147         switch (freshness) {
148             case PithosNodeStateFresh:
149                 break;
150             case PithosNodeStateRefreshNeeded:
151                 freshness = PithosNodeStateRefreshing;
152                 accountRequest = [ASIPithosAccountRequest listContainersRequestWithPithos:pithos 
153                                                                                      limit:0 
154                                                                                     marker:nil 
155                                                                                     shared:shared 
156                                                                                      until:nil];
157                 if (sharingAccount)
158                     [accountRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithos];
159                 else if (!forcedRefresh)
160                     accountRequest.downloadCache = [ASIDownloadCache sharedCache];
161                 accountRequest.delegate = self;
162                 accountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
163                 accountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
164                 accountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
165                                            [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
166                                            [NSNumber numberWithUnsignedInteger:10], @"retries", 
167                                            NSStringFromSelector(@selector(accountRequestFinished:)), @"didFinishSelector", 
168                                            NSStringFromSelector(@selector(accountRequestFailed:)), @"didFailSelector", 
169                                            nil];
170                 [[PithosUtilities prepareRequest:accountRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
171                 break;
172             case PithosNodeStateRefreshing:
173                 break;
174             case PithosNodeStateRefreshFinished:
175                 if (newChildren) {
176                     children = newChildren;
177                     newChildren = nil;
178                 }
179                 freshness = PithosNodeStateFresh;
180             default:
181                 break;
182         }
183         return children;
184     }
185 }
186
187 - (NSString *)displayName {
188     if (displayName == nil) {
189         if (!sharingAccount) {
190             return @"account";
191         } else if (pithosAccountManager) {
192             return [pithosAccountManager displaynameForUUID:sharingAccount safe:YES];
193         } else {
194             return [sharingAccount copy];
195         }
196     }
197     return [displayName copy];
198 }
199
200 - (NSImage *)icon {
201     if (icon == nil)
202         icon = sharedIcon;
203     return icon;
204 }
205
206 #pragma mark -
207 #pragma mark ASIHTTPRequestDelegate
208
209 - (void)accountRequestFailed:(ASIPithosAccountRequest *)request {
210     @autoreleasepool {
211         NSString *message;
212         NSError *error = [accountRequest error];
213         if (error)
214             message = [NSString stringWithFormat:@"Account listing %@ failed: %@", accountRequest.url, [error localizedDescription]];
215         else
216             message = [NSString stringWithFormat:@"Account listing %@ failed: (%d) %@", 
217                        accountRequest.url, accountRequest.responseStatusCode, accountRequest.responseStatusMessage];
218         dispatch_async(dispatch_get_main_queue(), ^{
219             [[PithosActivityFacility defaultPithosActivityFacility] startAndEndActivityWithType:PithosActivityOther message:message];
220         });
221         NSUInteger retries = [[accountRequest.userInfo objectForKey:@"retries"] unsignedIntegerValue];
222         if (retries > 0) {
223             ASIPithosAccountRequest *newAccountRequest = (ASIPithosAccountRequest *)[PithosUtilities copyRequest:accountRequest];
224             [(NSMutableDictionary *)(newAccountRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
225             accountRequest = newAccountRequest;
226             [[PithosUtilities prepareRequest:accountRequest priority:[[accountRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
227         } else {
228             newChildren = nil;
229             accountRequest = nil;
230             containers = nil;
231             forcedRefresh = NO;
232             @synchronized(self) {
233                 freshness = PithosNodeStateRefreshNeeded;
234             }
235         }
236     }
237 }
238
239 - (void)accountRequestFinished:(ASIPithosAccountRequest *)request {
240     @autoreleasepool {
241         DLog(@"List account finished: %@", [accountRequest url]);
242         DLog(@"Cached: %d", [accountRequest didUseCachedResponse]);
243         if (accountRequest.responseStatusCode == 200) {
244             self.pithosAccount = [accountRequest account];
245             
246             NSArray *someContainers = [accountRequest containers];
247             if (containers == nil) {
248                 containers = [[NSMutableArray alloc] initWithArray:someContainers];
249             } else {
250                 [containers addObjectsFromArray:someContainers];
251             }
252             if ([someContainers count] < 10000) {
253                 if (!accountRequest.didUseCachedResponse || ([containers count] != [someContainers count]) || !children) {
254                     // Save new children
255                     DLog(@"using newChildren");
256                     newChildren = [[NSMutableArray alloc] init];
257                     NSMutableIndexSet *keptNodes = [NSMutableIndexSet indexSet];
258                     for (ASIPithosContainer *container in containers) {
259                         PithosContainerNode *node = [[PithosContainerNode alloc] initWithPithos:pithos pithosContainer:container];
260                         node.parent = self;
261                         node.shared = shared;
262                         node.sharingAccount = sharingAccount;
263                         node.inheritChildrenUpdatedNotificationName = inheritChildrenUpdatedNotificationName;
264                         node.pithosAccountManager = pithosAccountManager;
265                         if (children) {
266                             NSUInteger oldIndex = [children indexOfObject:node];
267                             if (oldIndex != NSNotFound) {
268                                 // Use the same pointer value, if possible
269                                 node = [children objectAtIndex:oldIndex];
270     //                            node.pithosContainer = container;
271                                 [node setLimitedPithosContainer:container];
272                                 [keptNodes addIndex:oldIndex];
273                             }
274                         }
275                         [newChildren addObject:node];
276                     }
277                     [[children objectsAtIndexes:
278                       [[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [children count])] indexesPassingTest:^(NSUInteger idx, BOOL *stop){
279                         if ([keptNodes containsIndex:idx])
280                             return NO;
281                         return YES;
282                     }]] makeObjectsPerformSelector:@selector(pithosNodeWillBeRemoved)];
283                 }
284                 // Else cache was used and all results were fetched during this request, so existing children can be reused
285                 accountRequest = nil;
286                 containers = nil;
287                 forcedRefresh = NO;
288                 @synchronized(self) {
289                     freshness = PithosNodeStateRefreshFinished;
290                 }
291                 [self postChildrenUpdatedNotificationName];
292             } else {
293                 // Do an additional request to fetch more objects
294                 accountRequest = [ASIPithosAccountRequest listContainersRequestWithPithos:pithos 
295                                                                                      limit:0 
296                                                                                     marker:[[someContainers lastObject] name] 
297                                                                                     shared:shared 
298                                                                                      until:nil];
299                 if (sharingAccount)
300                     [accountRequest setRequestUserFromDefaultTo:sharingAccount withPithos:pithos];
301                 else if (!forcedRefresh)
302                     accountRequest.downloadCache = [ASIDownloadCache sharedCache];
303                 accountRequest.delegate = self;
304                 accountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
305                 accountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
306                 accountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
307                                            [NSNumber numberWithInteger:NSOperationQueuePriorityVeryHigh], @"priority", 
308                                            [NSNumber numberWithUnsignedInteger:10], @"retries", 
309                                            NSStringFromSelector(@selector(accountRequestFinished:)), @"didFinishSelector", 
310                                            NSStringFromSelector(@selector(accountRequestFailed:)), @"didFailSelector", 
311                                            nil];
312                 [[PithosUtilities prepareRequest:accountRequest priority:NSOperationQueuePriorityVeryHigh] startAsynchronous];
313             }
314         } else if (accountRequest.responseStatusCode == 304) {
315             // Account is not modified, so existing children can be reused
316             accountRequest = nil;
317             containers = nil;
318             forcedRefresh = NO;
319             @synchronized(self) {
320                 freshness = PithosNodeStateRefreshFinished;
321             }
322             [self postChildrenUpdatedNotificationName];
323         } else {
324             [self accountRequestFailed:accountRequest];
325         }
326     }
327 }
328
329 - (void)accountMetadataRequestFinished:(ASIPithosAccountRequest *)request {
330     @autoreleasepool {
331         DLog(@"URL: %@", [request url]);
332         DLog(@"cached: %d", [request didUseCachedResponse]);
333         
334         if ([request isEqualTo:applyMetadataAccountRequest]) {
335             @synchronized(self) {
336                 applyMetadataAccountRequest = nil;
337             }
338             [self refreshInfo];
339         } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
340             self.pithosAccount = [refreshMetadataAccountRequest account];
341             @synchronized(self) {
342                 refreshMetadataAccountRequest = nil;
343             }
344         }
345     }
346 }
347
348 - (void)accountMetadataRequestFailed:(ASIPithosAccountRequest *)request {
349     @autoreleasepool {
350         NSUInteger retries = [[request.userInfo objectForKey:@"retries"] unsignedIntegerValue];
351         if (retries > 0) {
352             ASIPithosAccountRequest *newRequest = (ASIPithosAccountRequest *)[PithosUtilities copyRequest:request];
353             [(NSMutableDictionary *)(newRequest.userInfo)setObject:[NSNumber numberWithUnsignedInteger:(--retries)] forKey:@"retries"];
354             if ([request isEqualTo:applyMetadataAccountRequest]) {
355                 @synchronized(self) {
356                     applyMetadataAccountRequest = newRequest;
357                 }
358             } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
359                 @synchronized(self) {
360                     refreshMetadataAccountRequest = newRequest;
361                 }
362             }
363             [[PithosUtilities prepareRequest:newRequest priority:[[newRequest.userInfo objectForKey:@"priority"] integerValue]] startAsynchronous];
364         } else {
365             if ([request isEqualTo:applyMetadataAccountRequest]) {
366                 [PithosUtilities httpRequestErrorAlertWithRequest:applyMetadataAccountRequest];
367                 @synchronized(self) {
368                     applyMetadataAccountRequest = nil;
369                 }
370             } else if ([request isEqualTo:refreshMetadataAccountRequest]) {
371                 [PithosUtilities httpRequestErrorAlertWithRequest:refreshMetadataAccountRequest];
372                 @synchronized(self) {
373                     refreshMetadataAccountRequest = nil;
374                 }
375             }
376         }
377     }
378 }
379
380 #pragma mark -
381 #pragma mark Info
382
383 - (void)applyInfo {
384     @synchronized(self) {
385         if (applyMetadataAccountRequest == nil) {
386             NSMutableDictionary *groups = [NSMutableDictionary dictionary];
387             if (translatedGroups.count) {
388                 for (NSString *groupName in translatedGroups) {
389                     if (!groupName.length ||
390                         [groupName rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" -_~,;"]].location != NSNotFound) {
391                         NSAlert *alert = [[NSAlert alloc] init];
392                         [alert setMessageText:@"Invalid Input"];
393                         [alert setInformativeText:@"Group names cannot be empty or contain ' ', '-', '_', '~', ',' or ';'."];
394                         [alert addButtonWithTitle:@"OK"];
395                         [alert runModal];
396                         return;
397                     }
398                 }
399                 if (pithosAccountManager) {
400                     NSMutableSet *allGroupUsers = [NSMutableSet set];
401                     for (NSMutableArray *groupUsers in [translatedGroups objectEnumerator]) {
402                         [allGroupUsers addObjectsFromArray:groupUsers];
403                     }
404                     [allGroupUsers removeObject:@""];
405                     [allGroupUsers removeObject:@"*"];
406                     if (allGroupUsers.count) {
407                         ASIPithosRequest *userCatalogRequest = [pithosAccountManager updateUserCatalogForForDisplaynames:[allGroupUsers allObjects]
408                                                                                                                         UUIDs:nil];
409                         if (userCatalogRequest.error || ((userCatalogRequest.responseStatusCode != 200) && (userCatalogRequest.responseStatusCode != 404))) {
410                             return;
411                         } else if (userCatalogRequest.responseStatusCode == 200) {
412                             // Check if all users exist.
413                             NSDictionary *displaynameCatalog = [userCatalogRequest displaynameCatalog];
414                             NSMutableArray *inexistentGroupUsers = [NSMutableArray array];
415                             for (NSString *groupUser in allGroupUsers) {
416                                 if (![displaynameCatalog objectForKey:groupUser]) {
417                                     [inexistentGroupUsers addObject:groupUser];
418                                 }
419                             }
420                             if (!inexistentGroupUsers.count) {
421                                 // create groups
422                                 for (NSString *groupName in translatedGroups) {
423                                     NSMutableArray *groupUsers = [NSMutableArray array];
424                                     for (NSString *groupUser in [translatedGroups objectForKey:groupName]) {
425                                         [groupUsers addObject:([groupUser isEqualToString:@"*"] ?
426                                                                @"*" : [displaynameCatalog objectForKey:groupUser])];
427                                     }
428                                     [groups setObject:groupUsers forKey:groupName];
429                                 }
430                             } else {
431                                 NSAlert *alert = [[NSAlert alloc] init];
432                                 if (inexistentGroupUsers.count == 1) {
433                                     [alert setMessageText:@"Invalid User"];
434                                     [alert setInformativeText:[NSString stringWithFormat:@"User '%@' doesn't exist.", [inexistentGroupUsers objectAtIndex:0]]];
435                                 } else {
436                                     [alert setMessageText:@"Invalid Users"];
437                                     [alert setInformativeText:[NSString stringWithFormat:@"Users '%@' don't exist.", [inexistentGroupUsers componentsJoinedByString:@"', '"]]];
438                                 }
439                                 [alert addButtonWithTitle:@"OK"];
440                                 [alert runModal];
441                                 return;
442                             }
443                         } else {
444                             // 404. Since we don't translate to UUIDs, check for invalid chars.
445                             BOOL valid = YES;
446                             for (NSString *groupUser in allGroupUsers) {
447                                 if ([groupUser rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@" ~,;:"]].location != NSNotFound) {
448                                     valid = NO;
449                                     break;
450                                 }
451                             }
452                             if (valid) {
453                                 [groups addEntriesFromDictionary:translatedGroups];
454                             } else {
455                                 NSAlert *alert = [[NSAlert alloc] init];
456                                 [alert setMessageText:@"Invalid Input"];
457                                 [alert setInformativeText:@"Users cannot contain ' ', '~', ',', ';' or ':'."];
458                                 [alert addButtonWithTitle:@"OK"];
459                                 [alert runModal];
460                                 return;
461                             }
462                         }
463                     } else {
464                         [groups setObject:[NSArray arrayWithObject:@""] forKey:@"group"];
465                     }
466                 } else {
467                     [groups addEntriesFromDictionary:translatedGroups];
468                 }
469             } else {
470                 [groups setObject:[NSArray arrayWithObject:@""] forKey:@"group"];
471             }
472             
473             applyMetadataAccountRequest = [ASIPithosAccountRequest updateAccountMetadataRequestWithPithos:pithos
474                                                                                                     groups:groups 
475                                                                                                   metadata:pithosAccount.metadata 
476                                                                                                     update:NO];
477             applyMetadataAccountRequest.delegate = self;
478             applyMetadataAccountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
479             applyMetadataAccountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
480             applyMetadataAccountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
481                                                     [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
482                                                     [NSNumber numberWithUnsignedInteger:10], @"retries", 
483                                                     NSStringFromSelector(@selector(accountMetadataRequestFinished:)), @"didFinishSelector", 
484                                                     NSStringFromSelector(@selector(accountMetadataRequestFailed:)), @"didFailSelector", 
485                                                     nil];
486             [[PithosUtilities prepareRequest:applyMetadataAccountRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
487         }
488     }
489 }
490
491 - (void)refreshInfo {
492     @synchronized(self) {
493         if (refreshMetadataAccountRequest == nil) {
494             refreshMetadataAccountRequest = [ASIPithosAccountRequest accountMetadataRequestWithPithos:pithos];
495             refreshMetadataAccountRequest.delegate = self;
496             refreshMetadataAccountRequest.didFinishSelector = @selector(performRequestFinishedDelegateInBackground:);
497             refreshMetadataAccountRequest.didFailSelector = @selector(performRequestFailedDelegateInBackground:);
498             refreshMetadataAccountRequest.userInfo = [NSMutableDictionary dictionaryWithObjectsAndKeys:
499                                                       [NSNumber numberWithInteger:NSOperationQueuePriorityHigh], @"priority", 
500                                                       [NSNumber numberWithUnsignedInteger:10], @"retries", 
501                                                       NSStringFromSelector(@selector(accountMetadataRequestFinished:)), @"didFinishSelector", 
502                                                       NSStringFromSelector(@selector(accountMetadataRequestFailed:)), @"didFailSelector", 
503                                                       nil];
504             if (!sharingAccount)
505                 refreshMetadataAccountRequest.downloadCache = [ASIDownloadCache sharedCache];
506             [[PithosUtilities prepareRequest:refreshMetadataAccountRequest priority:NSOperationQueuePriorityHigh] startAsynchronous];
507         }
508     }
509 }
510
511 @end