Reviewed by Vicki Murley
[WebKit-https.git] / WebKit / Misc.subproj / WebIconDatabase.m
1 /*
2  * Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import <WebKit/WebIconDatabase.h>
30
31 #import <WebKit/WebIconDatabasePrivate.h>
32 #import <WebKit/WebFileDatabase.h>
33 #import <WebKit/WebKitLogging.h>
34 #import <WebKit/WebNSURLExtras.h>
35 #import <WebKit/WebKitNSStringExtras.h>
36
37 NSString * const WebIconDatabaseVersionKey =    @"WebIconDatabaseVersion";
38 NSString * const WebURLToIconURLKey =           @"WebSiteURLToIconURLKey";
39
40 NSString * const ObsoleteIconsOnDiskKey =       @"WebIconsOnDisk";
41 NSString * const ObsoleteIconURLToURLsKey =     @"WebIconURLToSiteURLs";
42
43 static const int WebIconDatabaseCurrentVersion = 2;
44
45 NSString *WebIconDatabaseDidAddIconNotification =          @"WebIconDatabaseDidAddIconNotification";
46 NSString *WebIconNotificationUserInfoURLKey =              @"WebIconNotificationUserInfoURLKey";
47 NSString *WebIconDatabaseDidRemoveAllIconsNotification =   @"WebIconDatabaseDidRemoveAllIconsNotification";
48
49 NSString *WebIconDatabaseDirectoryDefaultsKey = @"WebIconDatabaseDirectoryDefaultsKey";
50 NSString *WebIconDatabaseEnabledDefaultsKey =   @"WebIconDatabaseEnabled";
51
52 NSSize WebIconSmallSize = {16, 16};
53 NSSize WebIconMediumSize = {32, 32};
54 NSSize WebIconLargeSize = {128, 128};
55
56 @interface NSMutableDictionary (WebIconDatabase)
57 - (void)_web_setObjectUsingSetIfNecessary:(id)object forKey:(id)key;
58 @end
59
60 @interface WebIconDatabase (WebInternal)
61 - (void)_clearDictionaries;
62 - (void)_createFileDatabase;
63 - (void)_loadIconDictionaries;
64 - (void)_updateFileDatabase;
65 - (void)_forgetIconForIconURLString:(NSString *)iconURLString;
66 - (NSMutableDictionary *)_iconsForIconURLString:(NSString *)iconURL;
67 - (NSImage *)_iconForFileURL:(NSString *)fileURL withSize:(NSSize)size;
68 - (void)_retainIconForIconURLString:(NSString *)iconURL;
69 - (void)_releaseIconForIconURLString:(NSString *)iconURL;
70 - (void)_retainOriginalIconsOnDisk;
71 - (void)_releaseOriginalIconsOnDisk;
72 - (void)_sendNotificationForURL:(NSString *)URL;
73 - (int)_totalRetainCountForIconURLString:(NSString *)iconURLString;
74 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons;
75 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon;
76 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache;
77 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size;
78 @end
79
80
81 @implementation WebIconDatabase
82
83 + (WebIconDatabase *)sharedIconDatabase
84 {
85     static WebIconDatabase *database = nil;
86     
87     if (!database) {
88 #if !LOG_DISABLED
89         double start = CFAbsoluteTimeGetCurrent();
90 #endif
91         database = [[WebIconDatabase alloc] init];
92 #if !LOG_DISABLED
93         LOG(Timing, "initializing icon database with %d sites and %d icons took %f", 
94             [database->_private->pageURLToIconURL count], [database->_private->iconURLToPageURLs count], (CFAbsoluteTimeGetCurrent() - start));
95 #endif
96     }
97     return database;
98 }
99
100 - init
101 {
102     [super init];
103     
104     _private = [[WebIconDatabasePrivate alloc] init];
105
106     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
107     NSDictionary *initialDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:[NSNumber numberWithBool:YES], WebIconDatabaseEnabledDefaultsKey, nil];
108     [defaults registerDefaults:initialDefaults];
109     [initialDefaults release];
110     if (![defaults boolForKey:WebIconDatabaseEnabledDefaultsKey]) {
111         return self;
112     }
113     
114     [self _createFileDatabase];
115     [self _loadIconDictionaries];
116
117     _private->iconURLToIcons = [[NSMutableDictionary alloc] init];
118     _private->iconURLToExtraRetainCount = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, NULL);
119     _private->pageURLToRetainCount = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, NULL);
120     _private->iconsToEraseWithURLs = [[NSMutableSet alloc] init];
121     _private->iconsToSaveWithURLs = [[NSMutableSet alloc] init];
122     _private->iconURLsWithNoIcons = [[NSMutableSet alloc] init];
123     
124     [[NSNotificationCenter defaultCenter] addObserver:self
125                                              selector:@selector(_applicationWillTerminate:)
126                                                  name:NSApplicationWillTerminateNotification
127                                                object:NSApp];
128
129     // Retain icons on disk then release them once clean-up has begun.
130     // This gives the client the opportunity to retain them before they are erased.
131     [self _retainOriginalIconsOnDisk];
132     [self performSelector:@selector(_releaseOriginalIconsOnDisk) withObject:nil afterDelay:0];
133     
134     return self;
135 }
136
137 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size cache:(BOOL)cache
138 {
139     ASSERT(size.width);
140     ASSERT(size.height);
141     
142     if (!URL || ![self _isEnabled]) {
143         return [self defaultIconWithSize:size];
144     }
145
146     if ([URL _webkit_isFileURL]) {
147         return [self _iconForFileURL:URL withSize:size];
148     }
149     
150     NSString *iconURLString = [_private->pageURLToIconURL objectForKey:URL];
151     if (!iconURLString) {
152         // Don't have it
153         return [self defaultIconWithSize:size];
154     }
155
156     NSMutableDictionary *icons = [self _iconsForIconURLString:iconURLString];
157     if (!icons) {
158         if (![_private->iconURLsWithNoIcons containsObject:iconURLString]) {
159             ERROR("WebIconDatabase said it had %@, but it doesn't.", iconURLString);
160         }
161         return [self defaultIconWithSize:size];
162     }        
163
164     return [self _iconFromDictionary:icons forSize:size cache:cache];
165 }
166
167 - (NSImage *)iconForURL:(NSString *)URL withSize:(NSSize)size
168 {
169     return [self iconForURL:URL withSize:size cache:YES];
170 }
171
172 - (NSString *)iconURLForURL:(NSString *)URL
173 {
174     if (![self _isEnabled]) {
175         return nil;
176     }
177     return URL ? [_private->pageURLToIconURL objectForKey:URL] : nil;
178 }
179
180 - (NSImage *)defaultIconWithSize:(NSSize)size
181 {
182     ASSERT(size.width);
183     ASSERT(size.height);
184     
185     if (!_private->defaultIcons) {
186         NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"url_icon" ofType:@"tiff"];
187         if (path) {
188             NSImage *icon = [[NSImage alloc] initByReferencingFile:path];
189             _private->defaultIcons = [[NSMutableDictionary dictionaryWithObject:icon
190                                             forKey:[NSValue valueWithSize:[icon size]]] retain];
191             [icon release];
192         }
193     }
194
195     return [self _iconFromDictionary:_private->defaultIcons forSize:size cache:YES];
196 }
197
198 - (void)retainIconForURL:(NSString *)URL
199 {
200     ASSERT(URL);
201     
202     if (![self _isEnabled]) {
203         return;
204     }
205     
206     int retainCount = (int)(void *)CFDictionaryGetValue(_private->pageURLToRetainCount, URL);
207     CFDictionarySetValue(_private->pageURLToRetainCount, URL, (void *)(retainCount + 1));
208 }
209
210 - (void)releaseIconForURL:(NSString *)pageURL
211 {
212     ASSERT(pageURL);
213     
214     if (![self _isEnabled]) {
215         return;
216     }    
217     
218     int retainCount = (int)(void *)CFDictionaryGetValue(_private->pageURLToRetainCount, pageURL);
219     
220     if (retainCount <= 0) {
221         ERROR("The icon for %@ was released more times than it was retained.", pageURL);
222         return;
223     }
224     
225     int newRetainCount = retainCount - 1;
226
227     if (newRetainCount == 0) {
228         // Forget association between this page URL and a retain count
229         CFDictionaryRemoveValue(_private->pageURLToRetainCount, pageURL);
230
231         // If there's a known iconURL for this page URL, we need to do more cleanup
232         NSString *iconURL = [_private->pageURLToIconURL objectForKey:pageURL];
233         if (iconURL != nil) {
234             // If there are no other retainers of this icon, forget it entirely
235             if ([self _totalRetainCountForIconURLString:iconURL] == 0) {
236                 [self _forgetIconForIconURLString:iconURL];
237             } else {
238                 // There's at least one other retainer of this icon, so we need to forget the
239                 // two-way links between this page URL and the icon URL, without blowing away
240                 // the icon entirely.                
241                 [_private->pageURLToIconURL removeObjectForKey:pageURL];
242             
243                 id pageURLs = [_private->iconURLToPageURLs objectForKey:iconURL];
244                 if ([pageURLs isKindOfClass:[NSMutableSet class]]) {
245                     ASSERT([pageURLs containsObject:pageURL]);
246                     [pageURLs removeObject:pageURL];
247                     
248                     // Maybe this was the last page URL mapped to this icon URL
249                     if ([pageURLs count] == 0) {
250                         [_private->iconURLToPageURLs removeObjectForKey:iconURL];
251                     }
252                 } else {
253                     // Only one page URL was associated with this icon URL; it must have been us
254                     ASSERT([pageURLs isKindOfClass:[NSString class]]);
255                     ASSERT([pageURLs isEqualToString:pageURL]);
256                     [_private->iconURLToPageURLs removeObjectForKey:pageURL];
257                 }
258             }
259         }
260     } else {
261         CFDictionarySetValue(_private->pageURLToRetainCount, pageURL, (void *)newRetainCount);
262     }
263 }
264
265 - (void)delayDatabaseCleanup
266 {
267     if (![self _isEnabled]) {
268         return;
269     }
270     
271     if(_private->didCleanup){
272         ERROR("delayDatabaseCleanup cannot be called after cleanup has begun");
273         return;
274     }
275     
276     _private->cleanupCount++;
277 }
278
279 - (void)allowDatabaseCleanup
280 {
281     if (![self _isEnabled]) {
282         return;
283     }
284     
285     if(_private->didCleanup){
286         ERROR("allowDatabaseCleanup cannot be called after cleanup has begun");
287         return;
288     }
289     
290     _private->cleanupCount--;
291
292     if(_private->cleanupCount == 0 && _private->waitingToCleanup){
293         [self _releaseOriginalIconsOnDisk];
294     }
295 }
296
297 @end
298
299
300 @implementation WebIconDatabase (WebPendingPublic)
301
302 - (void)removeAllIcons
303 {
304     NSEnumerator *keyEnumerator = [(NSDictionary *)_private->iconURLToPageURLs keyEnumerator];
305     NSString *iconURLString;
306     while ((iconURLString = [keyEnumerator nextObject]) != nil) {
307         // Note that _forgetIconForIconURLString does not affect retain counts, so the current clients
308         // need not do anything about retaining/releasing icons here. (However, the current clients should
309         // respond to WebIconDatabaseDidRemoveAllIconsNotification by refetching any icons that are 
310         // displayed in the UI.) 
311         [self _forgetIconForIconURLString:iconURLString];
312     }
313     
314     // Delete entire file database immediately. This has at least three advantages over waiting for
315     // _updateFileDatabase to execute:
316     // (1) _updateFileDatabase won't execute until an icon has been added
317     // (2) this is faster
318     // (3) this deletes all the on-disk hierarchy (especially useful if due to past problems there are
319     // some stale files in that hierarchy)
320     [_private->fileDatabase removeAllObjects];
321     [_private->iconsToEraseWithURLs removeAllObjects];
322     [_private->iconsToSaveWithURLs removeAllObjects];
323     [self _clearDictionaries];
324     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidRemoveAllIconsNotification
325                                                         object:self
326                                                       userInfo:nil];
327 }
328 @end
329
330 @implementation WebIconDatabase (WebPrivate)
331
332 - (BOOL)_isEnabled
333 {
334     return (_private->fileDatabase != nil);
335 }
336
337 - (void)_setIcon:(NSImage *)icon forIconURL:(NSString *)iconURL
338 {
339     ASSERT(icon);
340     ASSERT(iconURL);
341     ASSERT([self _isEnabled]);
342     
343     NSMutableDictionary *icons = [self _iconsBySplittingRepresentationsOfIcon:icon];
344     
345     if (!icons) {
346         return;
347     }
348     
349     [_private->iconURLToIcons setObject:icons forKey:iconURL];
350     
351     [self _retainIconForIconURLString:iconURL];
352     
353     // Release the newly created icon much like an autorelease.
354     // This gives the client enough time to retain it.
355     // FIXME: Should use an actual autorelease here using a proxy object instead.
356     [self performSelector:@selector(_releaseIconForIconURLString:) withObject:iconURL afterDelay:0];
357 }
358
359 - (void)_setHaveNoIconForIconURL:(NSString *)iconURL
360 {
361     ASSERT(iconURL);
362     ASSERT([self _isEnabled]);
363
364     [_private->iconURLsWithNoIcons addObject:iconURL];
365     
366     [self _retainIconForIconURLString:iconURL];
367     
368     // Release the newly created icon much like an autorelease.
369     // This gives the client enough time to retain it.
370     // FIXME: Should use an actual autorelease here using a proxy object instead.
371     [self performSelector:@selector(_releaseIconForIconURLString:) withObject:iconURL afterDelay:0];
372 }
373
374
375 - (void)_setIconURL:(NSString *)iconURL forURL:(NSString *)URL
376 {
377     ASSERT(iconURL);
378     ASSERT(URL);
379     ASSERT([self _isEnabled]);
380     ASSERT([self _hasIconForIconURL:iconURL]);
381  
382     if ([[_private->pageURLToIconURL objectForKey:URL] isEqualToString:iconURL] &&
383         [_private->iconsOnDiskWithURLs containsObject:iconURL]) {
384         // Don't do any work if the icon URL is already bound to the site URL
385         return;
386     }
387     
388     [_private->pageURLToIconURL setObject:iconURL forKey:URL];
389     [_private->iconURLToPageURLs _web_setObjectUsingSetIfNecessary:URL forKey:iconURL];
390         
391     [self _sendNotificationForURL:URL];
392     [self _updateFileDatabase];
393 }
394
395 - (BOOL)_hasIconForIconURL:(NSString *)iconURL;
396 {
397     ASSERT([self _isEnabled]);
398     
399     return (([_private->iconURLToIcons objectForKey:iconURL] ||
400              [_private->iconURLsWithNoIcons containsObject:iconURL] ||
401              [_private->iconsOnDiskWithURLs containsObject:iconURL]) &&
402              [self _totalRetainCountForIconURLString:iconURL] > 0);
403 }
404
405 @end
406
407 @implementation WebIconDatabase (WebInternal)
408
409 - (void)_createFileDatabase
410 {
411     // FIXME: Make defaults key public somehow
412     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
413     NSString *databaseDirectory = [defaults objectForKey:WebIconDatabaseDirectoryDefaultsKey];
414
415     if (!databaseDirectory) {
416         databaseDirectory = @"~/Library/Icons";
417         [defaults setObject:databaseDirectory forKey:WebIconDatabaseDirectoryDefaultsKey];
418     }
419     databaseDirectory = [databaseDirectory stringByExpandingTildeInPath];
420     
421     _private->fileDatabase = [[WebFileDatabase alloc] initWithPath:databaseDirectory];
422     [_private->fileDatabase setSizeLimit:20000000];
423     [_private->fileDatabase open];
424 }
425
426 - (void)_clearDictionaries
427 {
428     [_private->pageURLToIconURL release];
429     [_private->iconURLToPageURLs release];
430     [_private->iconsOnDiskWithURLs release];
431     [_private->originalIconsOnDiskWithURLs release];
432     _private->pageURLToIconURL = [[NSMutableDictionary alloc] init];
433     _private->iconURLToPageURLs = [[NSMutableDictionary alloc] init];
434     _private->iconsOnDiskWithURLs = [[NSMutableSet alloc] init];
435     _private->originalIconsOnDiskWithURLs = [[NSMutableSet alloc] init];
436 }
437
438 - (void)_loadIconDictionaries
439 {
440     WebFileDatabase *fileDB = _private->fileDatabase;
441     if (!fileDB) {
442         return;
443     }
444     
445     NSNumber *version = [fileDB objectForKey:WebIconDatabaseVersionKey];
446     int v = 0;
447     // no version means first version
448     if (version == nil) {
449         v = 1;
450     } else if ([version isKindOfClass:[NSNumber class]]) {
451         v = [version intValue];
452     }
453     
454     // Get the site URL to icon URL dictionary from the file DB.
455     NSMutableDictionary *pageURLToIconURL = nil;
456     if (v <= WebIconDatabaseCurrentVersion) {
457         pageURLToIconURL = [[fileDB objectForKey:WebURLToIconURLKey] retain];
458         // Remove the old unnecessary mapping files.
459         if (v < WebIconDatabaseCurrentVersion) {
460             [fileDB removeObjectForKey:ObsoleteIconsOnDiskKey];
461             [fileDB removeObjectForKey:ObsoleteIconURLToURLsKey];
462         }        
463     }
464     
465     if (![pageURLToIconURL isKindOfClass:[NSMutableDictionary class]]) {
466         [self _clearDictionaries];
467         return;
468     }
469
470     // Keep a set of icon URLs on disk so we know what we need to write out or remove.
471     NSMutableSet *iconsOnDiskWithURLs = [[NSMutableSet alloc] initWithArray:[pageURLToIconURL allValues]];
472
473     // Reverse pageURLToIconURL so we have an icon URL to page URLs dictionary. 
474     NSMutableDictionary *iconURLToPageURLs = [[NSMutableDictionary alloc] initWithCapacity:[_private->iconsOnDiskWithURLs count]];
475     NSEnumerator *enumerator = [pageURLToIconURL keyEnumerator];
476     NSString *URL;
477     while ((URL = [enumerator nextObject])) {
478         NSString *iconURL = (NSString *)[pageURLToIconURL objectForKey:URL];
479         if (![URL isKindOfClass:[NSString class]] || ![iconURL isKindOfClass:[NSString class]]) {
480             [self _clearDictionaries];
481             return;
482         }
483         [iconURLToPageURLs _web_setObjectUsingSetIfNecessary:URL forKey:iconURL];
484     }
485
486     _private->pageURLToIconURL = pageURLToIconURL;
487     _private->iconURLToPageURLs = iconURLToPageURLs;
488     _private->iconsOnDiskWithURLs = iconsOnDiskWithURLs;
489     _private->originalIconsOnDiskWithURLs = [iconsOnDiskWithURLs copy];
490 }
491
492 // Only called by _setIconURL:forKey:
493 - (void)_updateFileDatabase
494 {
495     if(_private->cleanupCount != 0){
496         return;
497     }
498
499     WebFileDatabase *fileDB = _private->fileDatabase;
500     if (!fileDB) {
501         return;
502     }
503
504     [fileDB setObject:[NSNumber numberWithInt:WebIconDatabaseCurrentVersion] forKey:WebIconDatabaseVersionKey];
505
506     // Erase icons that have been released that are on disk.
507     // Must remove icons before writing them to disk or else we could potentially remove the newly written ones.
508     NSEnumerator *enumerator = [_private->iconsToEraseWithURLs objectEnumerator];
509     NSString *iconURLString;
510     
511     while ((iconURLString = [enumerator nextObject]) != nil) {
512         [fileDB removeObjectForKey:iconURLString];
513         [_private->iconsOnDiskWithURLs removeObject:iconURLString];
514     }
515
516     // Save icons that have been retained that are not already on disk
517     enumerator = [_private->iconsToSaveWithURLs objectEnumerator];
518
519     while ((iconURLString = [enumerator nextObject]) != nil) {
520         NSMutableDictionary *icons = [_private->iconURLToIcons objectForKey:iconURLString];
521         if (icons) {
522             // Save the 16 x 16 size icons as this is the only size that Safari uses.
523             // If we ever use larger sizes, we should save the largest size so icons look better when scaling up.
524             // This also works around the problem with cnet's blank 32x32 icon (3105486).
525             NSImage *icon = [icons objectForKey:[NSValue valueWithSize:NSMakeSize(16,16)]];
526             if (!icon) {
527                 // In case there is no 16 x 16 size.
528                 icon = [self _largestIconFromDictionary:icons];
529             }
530             NSData *iconData = [icon TIFFRepresentation];
531             if (iconData) {
532                 //NSLog(@"Writing icon: %@", iconURLString);
533                 [fileDB setObject:iconData forKey:iconURLString];
534                 [_private->iconsOnDiskWithURLs addObject:iconURLString];
535             }
536         } else if ([_private->iconURLsWithNoIcons containsObject:iconURLString]) {
537             [fileDB setObject:[NSNull null] forKey:iconURLString];
538             [_private->iconsOnDiskWithURLs addObject:iconURLString];
539         }
540     }
541     
542     [_private->iconsToEraseWithURLs removeAllObjects];
543     [_private->iconsToSaveWithURLs removeAllObjects];
544
545     // Save the icon dictionaries to disk. Save them as mutable copies otherwise WebFileDatabase may access the 
546     // same dictionaries on a separate thread as it's being modified. We think this fixes 3566336.
547     NSMutableDictionary *pageURLToIconURLCopy = [_private->pageURLToIconURL mutableCopy];
548     [fileDB setObject:pageURLToIconURLCopy forKey:WebURLToIconURLKey];
549     [pageURLToIconURLCopy release];
550 }
551
552 - (void)_applicationWillTerminate:(NSNotification *)notification
553 {
554     // Should only cause a write if user quit before 3 seconds after the last _updateFileDatabase
555     [_private->fileDatabase sync];
556 }
557
558 - (int)_totalRetainCountForIconURLString:(NSString *)iconURLString
559 {
560     // Add up the retain counts for each associated page, plus the retain counts not associated
561     // with any page, which are stored in _private->iconURLToExtraRetainCount
562     int result = (int)(void *)CFDictionaryGetValue(_private->iconURLToExtraRetainCount, iconURLString);
563     
564     id URLStrings = [_private->iconURLToPageURLs objectForKey:iconURLString];
565     if (URLStrings != nil) {
566         if ([URLStrings isKindOfClass:[NSMutableSet class]]) {
567             NSEnumerator *e = [(NSMutableSet *)URLStrings objectEnumerator];
568             NSString *URLString;
569             while ((URLString = [e nextObject]) != nil) {
570                 ASSERT([URLString isKindOfClass:[NSString class]]);
571                 result += (int)(void *)CFDictionaryGetValue(_private->pageURLToRetainCount, URLString);
572             }
573         } else {
574             ASSERT([URLStrings isKindOfClass:[NSString class]]);
575             result += (int)(void *)CFDictionaryGetValue(_private->pageURLToRetainCount, URLStrings);
576         }
577     }
578
579     return result;
580 }
581
582 - (NSMutableDictionary *)_iconsForIconURLString:(NSString *)iconURLString
583 {
584     ASSERT(iconURLString);
585
586     if ([_private->iconURLsWithNoIcons containsObject:iconURLString]) {
587         return nil;
588     }
589     
590     NSMutableDictionary *icons = [_private->iconURLToIcons objectForKey:iconURLString];
591
592     if (icons) {
593         return icons;
594     }
595         
596     // Not in memory, check disk
597     if(![_private->iconsOnDiskWithURLs containsObject:iconURLString]){
598         return nil;
599     }
600
601     
602 #if !LOG_DISABLED         
603     double start = CFAbsoluteTimeGetCurrent();
604 #endif
605     NSData *iconData = [_private->fileDatabase objectForKey:iconURLString];
606
607     if ([iconData isKindOfClass:[NSNull class]]) {
608         [_private->iconURLsWithNoIcons addObject:iconURLString];
609         return nil;
610     }
611     
612     if (iconData) {
613         NS_DURING
614             NSImage *icon = [[NSImage alloc] initWithData:iconData];
615             icons = [self _iconsBySplittingRepresentationsOfIcon:icon];
616             if (icons) {
617 #if !LOG_DISABLED 
618                 double duration = CFAbsoluteTimeGetCurrent() - start;
619                 LOG(Timing, "loading and creating icon %@ took %f seconds", iconURLString, duration);
620 #endif
621                 [_private->iconURLToIcons setObject:icons forKey:iconURLString];
622             }
623         NS_HANDLER
624             icons = nil;
625         NS_ENDHANDLER
626     }
627     
628     return icons;
629 }
630
631 - (NSImage *)_iconForFileURL:(NSString *)file withSize:(NSSize)size
632 {
633     ASSERT(size.width);
634     ASSERT(size.height);
635
636     NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
637     NSString *path = [[NSURL _web_URLWithDataAsString:file] path];
638     NSString *suffix = [path pathExtension];
639     NSImage *icon = nil;
640     
641     if ([suffix _webkit_isCaseInsensitiveEqualToString:@"htm"] || [suffix _webkit_isCaseInsensitiveEqualToString:@"html"]) {
642         if (!_private->htmlIcons) {
643             icon = [workspace iconForFileType:@"html"];
644             _private->htmlIcons = [[self _iconsBySplittingRepresentationsOfIcon:icon] retain];
645         }
646         icon = [self _iconFromDictionary:_private->htmlIcons forSize:size cache:YES];
647     } else {
648         if (!path || ![path isAbsolutePath]) {
649             // Return the generic icon when there is no path.
650             icon = [workspace iconForFileType:@"????"];
651         } else {
652             icon = [workspace iconForFile:path];
653         }
654         [self _scaleIcon:icon toSize:size];
655     }
656
657     return icon;
658 }
659
660 - (void)_retainIconForIconURLString:(NSString *)iconURLString
661 {
662     ASSERT(iconURLString);
663     
664     int retainCount = (int)(void *)CFDictionaryGetValue(_private->iconURLToExtraRetainCount, iconURLString);
665     int newRetainCount = retainCount + 1;
666
667     CFDictionarySetValue(_private->iconURLToExtraRetainCount, iconURLString, (void *)newRetainCount);
668
669     if (newRetainCount == 1 && ![_private->iconsOnDiskWithURLs containsObject:iconURLString]){
670         [_private->iconsToSaveWithURLs addObject:iconURLString];
671         [_private->iconsToEraseWithURLs removeObject:iconURLString];
672     }
673 }
674
675 - (void)_forgetIconForIconURLString:(NSString *)iconURLString
676 {
677     ASSERT_ARG(iconURLString, iconURLString != nil);
678     if([_private->iconsOnDiskWithURLs containsObject:iconURLString]){
679         [_private->iconsToEraseWithURLs addObject:iconURLString];
680         [_private->iconsToSaveWithURLs removeObject:iconURLString];
681     }
682     
683     // Remove the icon's images
684     [_private->iconURLToIcons removeObjectForKey:iconURLString];
685     
686     // Remove negative cache item for icon, if any
687     [_private->iconURLsWithNoIcons removeObject:iconURLString];
688     
689     // Remove the icon's associated site URLs, if any
690     [iconURLString retain];
691     id URLs = [_private->iconURLToPageURLs objectForKey:iconURLString];
692     if (URLs != nil) {
693         if ([URLs isKindOfClass:[NSMutableSet class]]) {
694             [_private->pageURLToIconURL removeObjectsForKeys:[URLs allObjects]];
695         } else {
696             ASSERT([URLs isKindOfClass:[NSString class]]);
697             [_private->pageURLToIconURL removeObjectForKey:URLs];
698         }
699     }
700     [_private->iconURLToPageURLs removeObjectForKey:iconURLString];
701     [iconURLString release];
702 }
703
704 - (void)_releaseIconForIconURLString:(NSString *)iconURLString
705 {
706     ASSERT(iconURLString);
707     
708     int retainCount = (int)(void *)CFDictionaryGetValue(_private->iconURLToExtraRetainCount, iconURLString);
709
710     if (retainCount <= 0) {
711         ASSERT_NOT_REACHED();
712         return;
713     }
714     
715     int newRetainCount = retainCount - 1;
716     if (newRetainCount == 0) {
717         CFDictionaryRemoveValue(_private->iconURLToExtraRetainCount, iconURLString);
718         if ([self _totalRetainCountForIconURLString:iconURLString] == 0) {
719             [self _forgetIconForIconURLString:iconURLString];
720         }
721     } else {
722         CFDictionarySetValue(_private->iconURLToExtraRetainCount, iconURLString, (void *)newRetainCount);
723     }
724 }
725
726 - (void)_retainOriginalIconsOnDisk
727 {
728     NSEnumerator *enumerator = [_private->originalIconsOnDiskWithURLs objectEnumerator];;
729     NSString *iconURLString;
730     while ((iconURLString = [enumerator nextObject]) != nil) {
731         [self _retainIconForIconURLString:iconURLString];
732     }
733 }
734
735 - (void)_releaseOriginalIconsOnDisk
736 {
737     if (_private->cleanupCount > 0) {
738         _private->waitingToCleanup = YES;
739         return;
740     }
741
742     NSEnumerator *enumerator = [_private->originalIconsOnDiskWithURLs objectEnumerator];
743     NSString *iconURLString;
744     while ((iconURLString = [enumerator nextObject]) != nil) {
745         [self _releaseIconForIconURLString:iconURLString];
746     }
747     
748     [_private->originalIconsOnDiskWithURLs release];
749     _private->originalIconsOnDiskWithURLs = nil;
750
751     _private->didCleanup = YES;
752 }
753
754 - (void)_sendNotificationForURL:(NSString *)URL
755 {
756     ASSERT(URL);
757     
758     NSDictionary *userInfo = [NSDictionary dictionaryWithObject:URL
759                                                          forKey:WebIconNotificationUserInfoURLKey];
760     [[NSNotificationCenter defaultCenter] postNotificationName:WebIconDatabaseDidAddIconNotification
761                                                         object:self
762                                                       userInfo:userInfo];
763 }
764
765 - (NSImage *)_largestIconFromDictionary:(NSMutableDictionary *)icons
766 {
767     ASSERT(icons);
768     
769     NSEnumerator *enumerator = [icons keyEnumerator];
770     NSValue *currentSize, *largestSize=nil;
771     float largestSizeArea=0;
772
773     while ((currentSize = [enumerator nextObject]) != nil) {
774         NSSize currentSizeSize = [currentSize sizeValue];
775         float currentSizeArea = currentSizeSize.width * currentSizeSize.height;
776         if(!largestSizeArea || (currentSizeArea > largestSizeArea)){
777             largestSize = currentSize;
778             largestSizeArea = currentSizeArea;
779         }
780     }
781
782     return [icons objectForKey:largestSize];
783 }
784
785 - (NSMutableDictionary *)_iconsBySplittingRepresentationsOfIcon:(NSImage *)icon
786 {
787     ASSERT(icon);
788
789     NSMutableDictionary *icons = [NSMutableDictionary dictionary];
790     NSEnumerator *enumerator = [[icon representations] objectEnumerator];
791     NSImageRep *rep;
792
793     while ((rep = [enumerator nextObject]) != nil) {
794         NSSize size = [rep size];
795         NSImage *subIcon = [[NSImage alloc] initWithSize:size];
796         [subIcon addRepresentation:rep];
797         [icons setObject:subIcon forKey:[NSValue valueWithSize:size]];
798         [subIcon release];
799     }
800
801     if([icons count] > 0){
802         return icons;
803     }
804
805     ERROR("icon has no representations");
806     
807     return nil;
808 }
809
810 - (NSImage *)_iconFromDictionary:(NSMutableDictionary *)icons forSize:(NSSize)size cache:(BOOL)cache
811 {
812     ASSERT(size.width);
813     ASSERT(size.height);
814
815     NSImage *icon = [icons objectForKey:[NSValue valueWithSize:size]];
816
817     if(!icon){
818         icon = [[[self _largestIconFromDictionary:icons] copy] autorelease];
819         [self _scaleIcon:icon toSize:size];
820
821         if(cache){
822             [icons setObject:icon forKey:[NSValue valueWithSize:size]];
823         }
824     }
825
826     return icon;
827 }
828
829 - (void)_scaleIcon:(NSImage *)icon toSize:(NSSize)size
830 {
831     ASSERT(size.width);
832     ASSERT(size.height);
833     
834 #if !LOG_DISABLED        
835     double start = CFAbsoluteTimeGetCurrent();
836 #endif
837     
838     [icon setScalesWhenResized:YES];
839     [icon setSize:size];
840     
841 #if !LOG_DISABLED
842     double duration = CFAbsoluteTimeGetCurrent() - start;
843     LOG(Timing, "scaling icon took %f seconds.", duration);
844 #endif
845 }
846
847 @end
848
849 @implementation WebIconDatabasePrivate
850
851 @end
852
853 @implementation NSMutableDictionary (WebIconDatabase)
854
855 - (void)_web_setObjectUsingSetIfNecessary:(id)object forKey:(id)key
856 {
857     id previousObject = [self objectForKey:key];
858     if (previousObject == nil) {
859         [self setObject:object forKey:key];
860     } else if ([previousObject isKindOfClass:[NSMutableSet class]]) {
861         [previousObject addObject:object];
862     } else {
863         NSMutableSet *objects = [[NSMutableSet alloc] initWithObjects:previousObject, object, nil];
864         [self setObject:objects forKey:key];
865         [objects release];
866     }
867 }
868
869 @end