-
Notifications
You must be signed in to change notification settings - Fork 10
/
SLSMoleculeAppDelegate.m
613 lines (503 loc) · 20.8 KB
/
SLSMoleculeAppDelegate.m
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
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
//
// SLSMoleculesAppDelegate.m
// Molecules
//
// The source code for Molecules is available under a BSD license. See License.txt for details.
//
// Created by Brad Larson on 5/18/2008.
//
// This is the base application delegate, used for handling launch, termination, and memory-related delegate methods
#import "SLSMoleculeAppDelegate.h"
#import "SLSMoleculeRootViewController.h"
#import "SLSMoleculeiPadRootViewController.h"
#import "SLSMolecule.h"
#import "NSData+Gzip.h"
#import "VCTitleCase.h"
#define MOLECULES_DATABASE_VERSION 1
@implementation SLSMoleculeAppDelegate
@synthesize window;
@synthesize rootViewController;
#pragma mark -
#pragma mark Initialization / teardown
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//Initialize the application window
window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
if (!window)
{
[self release];
return NO;
}
window.backgroundColor = [UIColor blackColor];
if ([SLSMoleculeAppDelegate isRunningOniPad])
{
UISplitViewController *newSplitViewController = [[UISplitViewController alloc] init];
rootViewController = [[SLSMoleculeiPadRootViewController alloc] init];
[rootViewController loadView];
newSplitViewController.viewControllers = [NSArray arrayWithObjects:rootViewController.tableNavigationController, rootViewController, nil];
newSplitViewController.delegate = (SLSMoleculeiPadRootViewController *)rootViewController;
splitViewController = newSplitViewController;
[window addSubview:splitViewController.view];
}
else
{
rootViewController = [[SLSMoleculeRootViewController alloc] init];
[window addSubview:rootViewController.view];
}
[window makeKeyAndVisible];
[window layoutSubviews];
// Start the initialization of the database, if necessary
isHandlingCustomURLMoleculeDownload = NO;
downloadedFileContents = nil;
initialDatabaseLoadLock = [[NSLock alloc] init];
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
NSURL *url = (NSURL *)[launchOptions valueForKey:UIApplicationLaunchOptionsURLKey];
if (url != nil)
isHandlingCustomURLMoleculeDownload = YES;
[self performSelectorInBackground:@selector(loadInitialMoleculesFromDisk) withObject:nil];
// Handle the Molecules custom URL scheme
[self handleCustomURLScheme:url];
return YES;
}
- (void)applicationWillTerminate:(UIApplication *)application
{
[rootViewController cancelMoleculeLoading];
[self disconnectFromDatabase];
}
- (void)dealloc
{
[splitViewController release];
[initialDatabaseLoadLock release];
[molecules release];
[rootViewController release];
[window release];
[super dealloc];
}
#pragma mark -
#pragma mark Device-specific interface control
/*+ (BOOL)isRunningOniPad;
{
return (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad);
}*/
+ (BOOL)isRunningOniPad;
{
static BOOL hasCheckediPadStatus = NO;
static BOOL isRunningOniPad = NO;
if (!hasCheckediPadStatus)
{
if ([[UIDevice currentDevice] respondsToSelector:@selector(userInterfaceIdiom)])
{
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)
{
isRunningOniPad = YES;
hasCheckediPadStatus = YES;
return isRunningOniPad;
}
}
hasCheckediPadStatus = YES;
}
return isRunningOniPad;
}
#pragma mark -
#pragma mark Database access
- (NSString *)applicationSupportDirectory;
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:basePath] == NO)
{
[fileManager createDirectoryAtPath:basePath attributes: nil];
}
return basePath;
}
- (BOOL)createEditableCopyOfDatabaseIfNeeded;
{
// First, see if the database exists in the /Documents directory. If so, move it to Application Support.
NSFileManager *fileManager = [NSFileManager defaultManager];
NSError *error;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *writableDBPath = [documentsDirectory stringByAppendingPathComponent:@"molecules.sql"];
if ([fileManager fileExistsAtPath:writableDBPath])
{
[fileManager moveItemAtPath:writableDBPath toPath:[[self applicationSupportDirectory] stringByAppendingPathComponent:@"molecules.sql"] error:&error];
}
writableDBPath = [[self applicationSupportDirectory] stringByAppendingPathComponent:@"molecules.sql"];
if ([fileManager fileExistsAtPath:writableDBPath])
return NO;
// The database does not exist, so copy a blank starter database to the Documents directory
NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"molecules.sql"];
BOOL success = [fileManager copyItemAtPath:defaultDBPath toPath:writableDBPath error:&error];
if (!success) {
NSAssert1(0,NSLocalizedStringFromTable(@"Failed to create writable database file with message '%@'.", @"Localized", nil), [error localizedDescription]);
}
return YES;
}
- (void)connectToDatabase;
{
molecules = [[NSMutableArray alloc] init];
// The database is stored in the application bundle.
NSString *path = [[self applicationSupportDirectory] stringByAppendingPathComponent:@"molecules.sql"];
// Open the database. The database was prepared outside the application.
if (sqlite3_open([path UTF8String], &database) == SQLITE_OK)
{
}
else
{
// Even though the open failed, call close to properly clean up resources.
sqlite3_close(database);
NSAssert1(0,NSLocalizedStringFromTable(@"Failed to open database with message '%s'.", @"Localized", nil), sqlite3_errmsg(database));
// Additional error handling, as appropriate...
}
}
- (void)disconnectFromDatabase;
{
// TODO: Maybe write out all database entries to disk
// [books makeObjectsPerformSelector:@selector(dehydrate)];
[SLSMolecule finalizeStatements];
// Close the database.
if (sqlite3_close(database) != SQLITE_OK)
{
//NSAssert1(0,NSLocalizedStringFromTable(@"Error: failed to close database with message '%s'.", @"Localized", nil), sqlite3_errmsg(database));
}
}
- (void)loadInitialMoleculesFromDisk;
{
[initialDatabaseLoadLock lock];
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
rootViewController.molecules = nil;
if ([self createEditableCopyOfDatabaseIfNeeded])
{
// The database needed to be recreated, so scan and copy over the default files
[self performSelectorOnMainThread:@selector(showStatusIndicator) withObject:nil waitUntilDone:NO];
[self connectToDatabase];
// Before anything else, move included PDB files to /Documents if the program hasn't been run before
// User might have intentionally deleted files, so don't recopy the files in that case
NSError *error = nil;
// Grab the /Documents directory path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSFileManager *fileManager = [NSFileManager defaultManager];
// Iterate through all files sitting in the application's Resources directory
// TODO: Can you fast enumerate this?
NSDirectoryEnumerator *direnum = [fileManager enumeratorAtPath:[[NSBundle mainBundle] resourcePath]];
NSString *pname;
while (pname = [direnum nextObject])
{
if ([[pname pathExtension] isEqualToString:@"gz"])
{
NSString *preloadedPDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:pname];
NSString *installedPDBPath = [documentsDirectory stringByAppendingPathComponent:pname];
if (![fileManager fileExistsAtPath:installedPDBPath])
{
// Move included PDB files to /Documents
[[NSFileManager defaultManager] copyItemAtPath:preloadedPDBPath toPath:installedPDBPath error:&error];
if (error != nil)
{
// NSLog(@"Failed to copy over PDB files with error: '%@'.", [error localizedDescription]);
// TODO: Report the file copying problem to the user or do something about it
}
}
}
}
[self loadMissingMoleculesIntoDatabase];
[[NSUserDefaults standardUserDefaults] synchronize];
[self performSelectorOnMainThread:@selector(hideStatusIndicator) withObject:nil waitUntilDone:YES];
}
else
{
// The MySQL database has been created, so load molecules from the database
[self connectToDatabase];
// TODO: Check to make sure that the proper version of the database is installed
[self loadAllMoleculesFromDatabase];
[self loadMissingMoleculesIntoDatabase];
}
rootViewController.database = database;
rootViewController.molecules = molecules;
[initialDatabaseLoadLock unlock];
if ([SLSMoleculeAppDelegate isRunningOniPad])
{
[[rootViewController.tableViewController tableView] performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO];
}
if (!isHandlingCustomURLMoleculeDownload)
[rootViewController loadInitialMolecule];
[pool release];
}
- (void)loadAllMoleculesFromDatabase;
{
const char *sql = "SELECT * FROM molecules";
sqlite3_stmt *moleculeLoadingStatement;
if (sqlite3_prepare_v2(database, sql, -1, &moleculeLoadingStatement, NULL) == SQLITE_OK)
{
while (sqlite3_step(moleculeLoadingStatement) == SQLITE_ROW)
{
SLSMolecule *newMolecule = [[SLSMolecule alloc] initWithSQLStatement:moleculeLoadingStatement database:database];
if (newMolecule != nil)
[molecules addObject:newMolecule];
[newMolecule release];
}
}
// "Finalize" the statement - releases the resources associated with the statement.
sqlite3_finalize(moleculeLoadingStatement);
}
- (void)loadMissingMoleculesIntoDatabase;
{
// First, load all molecule names from the database
NSMutableDictionary *moleculeFilenameLookupTable = [[NSMutableDictionary alloc] init];
const char *sql = "SELECT * FROM molecules";
sqlite3_stmt *moleculeLoadingStatement;
if (sqlite3_prepare_v2(database, sql, -1, &moleculeLoadingStatement, NULL) == SQLITE_OK)
{
while (sqlite3_step(moleculeLoadingStatement) == SQLITE_ROW)
{
char *stringResult = (char *)sqlite3_column_text(moleculeLoadingStatement, 1);
NSString *sqlString = (stringResult) ? [NSString stringWithUTF8String:stringResult] : @"";
NSString *filename = [sqlString stringByReplacingOccurrencesOfString:@"''" withString:@"'"];
[moleculeFilenameLookupTable setValue:[NSNumber numberWithBool:YES] forKey:filename];
}
}
sqlite3_finalize(moleculeLoadingStatement);
// Now, check all the files on disk to see if any are missing from the database
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSDirectoryEnumerator *direnum = [[NSFileManager defaultManager]
enumeratorAtPath:documentsDirectory];
NSString *pname;
while (pname = [direnum nextObject])
{
NSString *lastPathComponent = [pname lastPathComponent];
if (![lastPathComponent isEqualToString:pname])
{
NSError *error = nil;
// The file has been passed in using a subdirectory, so move it into the flattened /Documents directory
[[NSFileManager defaultManager] moveItemAtPath:[documentsDirectory stringByAppendingPathComponent:pname] toPath:[documentsDirectory stringByAppendingPathComponent:lastPathComponent] error:&error];
[[NSFileManager defaultManager] removeItemAtPath:[documentsDirectory stringByAppendingPathComponent:[pname stringByDeletingLastPathComponent]] error:&error];
pname = lastPathComponent;
}
if ( ([moleculeFilenameLookupTable valueForKey:pname] == nil) && ([[pname pathExtension] isEqualToString:@"gz"] || [[pname pathExtension] isEqualToString:@"pdb"]) )
{
// Parse the PDB file into the database
SLSMolecule *newMolecule = [[SLSMolecule alloc] initWithFilename:pname database:database];
if (newMolecule != nil)
[molecules addObject:newMolecule];
[newMolecule release];
}
}
[moleculeFilenameLookupTable release];
}
#pragma mark -
#pragma mark Status update methods
- (void)showStatusIndicator;
{
[[NSNotificationCenter defaultCenter] postNotificationName:@"FileLoadingStarted" object:NSLocalizedStringFromTable(@"Initializing database...", @"Localized", nil)];
}
- (void)showDownloadIndicator;
{
[[NSNotificationCenter defaultCenter] postNotificationName:@"FileLoadingStarted" object:NSLocalizedStringFromTable(@"Downloading molecule...", @"Localized", nil)];
}
- (void)updateStatusIndicator;
{
}
- (void)hideStatusIndicator;
{
[[NSNotificationCenter defaultCenter] postNotificationName:@"FileLoadingEnded" object:nil];
}
#pragma mark -
#pragma mark Flow control
- (void)applicationWillResignActive:(UIApplication *)application
{
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
}
#pragma mark -
#pragma mark Custom molecule download methods
- (BOOL)handleCustomURLScheme:(NSURL *)url;
{
if (url != nil)
{
isHandlingCustomURLMoleculeDownload = YES;
[NSThread sleepForTimeInterval:0.5]; // Wait for database to load
NSString *pathComponentForCustomURL = [[url host] stringByAppendingString:[url path]];
NSString *locationOfRemotePDBFile = [NSString stringWithFormat:@"http://%@", pathComponentForCustomURL];
nameOfDownloadedMolecule = [[pathComponentForCustomURL lastPathComponent] retain];
// Check to make sure that the file has not already been downloaded, if so, just switch to it
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
[initialDatabaseLoadLock lock];
if ([[NSFileManager defaultManager] fileExistsAtPath:[documentsDirectory stringByAppendingPathComponent:nameOfDownloadedMolecule]])
{
NSInteger indexForMoleculeMatchingThisName = 0, currentIndex = 0;
for (SLSMolecule *currentMolecule in molecules)
{
if ([[currentMolecule filename] isEqualToString:nameOfDownloadedMolecule])
{
indexForMoleculeMatchingThisName = currentIndex;
break;
}
currentIndex++;
}
[rootViewController selectedMoleculeDidChange:indexForMoleculeMatchingThisName];
[rootViewController loadInitialMolecule];
[nameOfDownloadedMolecule release];
nameOfDownloadedMolecule = nil;
return YES;
}
[initialDatabaseLoadLock unlock];
[rootViewController cancelMoleculeLoading];
[NSThread sleepForTimeInterval:0.1]; // Wait for cancel action to take place
// Determine if this is a file being passed in, or something to download
if ([url isFileURL])
{
[nameOfDownloadedMolecule release];
nameOfDownloadedMolecule = nil;
}
else
{
downloadCancelled = NO;
// Start download of new molecule
[self showDownloadIndicator];
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:locationOfRemotePDBFile]
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:60.0f];
downloadConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (downloadConnection)
{
downloadedFileContents = [[NSMutableData data] retain];
}
else
{
// inform the user that the download could not be made
return NO;
}
}
}
return YES;
}
- (void)downloadCompleted;
{
[downloadConnection release];
downloadConnection = nil;
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
[downloadedFileContents release];
downloadedFileContents = nil;
[self hideStatusIndicator];
[nameOfDownloadedMolecule release];
nameOfDownloadedMolecule = nil;
}
- (void)saveMoleculeWithData:(NSData *)moleculeData toFilename:(NSString *)filename;
{
[initialDatabaseLoadLock lock];
if (moleculeData != nil)
{
// Add the new protein to the list by gunzipping the data and pulling out the title
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSError *error = nil;
BOOL writeStatus;
if (isGzipCompressionUsedOnDownload)
{
writeStatus = [moleculeData writeToFile:[documentsDirectory stringByAppendingPathComponent:filename] options:NSAtomicWrite error:&error];
// writeStatus = [[moleculeData gzipDeflate] writeToFile:[documentsDirectory stringByAppendingPathComponent:filename] options:NSAtomicWrite error:&error];
// NSLog(@"Decompressing");
}
else
writeStatus = [moleculeData writeToFile:[documentsDirectory stringByAppendingPathComponent:filename] options:NSAtomicWrite error:&error];
if (!writeStatus)
{
// TODO: Do some error handling here
return;
}
SLSMolecule *newMolecule = [[SLSMolecule alloc] initWithFilename:filename database:database];
if (newMolecule == nil)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedStringFromTable(@"Error in downloaded file", @"Localized", nil) message:NSLocalizedStringFromTable(@"The molecule file is either corrupted or not of a supported format", @"Localized", nil)
delegate:self cancelButtonTitle:NSLocalizedStringFromTable(@"OK", @"Localized", nil) otherButtonTitles: nil];
[alert show];
[alert release];
// Delete the corrupted or sunsupported file
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSError *error = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:[documentsDirectory stringByAppendingPathComponent:filename] error:&error])
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedStringFromTable(@"Could not delete file", @"Localized", nil) message:[error localizedDescription]
delegate:self cancelButtonTitle:NSLocalizedStringFromTable(@"OK", @"Localized", nil) otherButtonTitles: nil];
[alert show];
[alert release];
return;
}
}
else
{
[molecules addObject:newMolecule];
[newMolecule release];
[rootViewController updateTableListOfMolecules];
[rootViewController selectedMoleculeDidChange:([molecules count] - 1)];
[rootViewController loadInitialMolecule];
}
}
[initialDatabaseLoadLock unlock];
}
#pragma mark -
#pragma mark URL connection delegate methods
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedStringFromTable(@"Connection failed", @"Localized", nil) message:NSLocalizedStringFromTable(@"Could not connect to the Protein Data Bank", @"Localized", nil)
delegate:self cancelButtonTitle:NSLocalizedStringFromTable(@"OK", @"Localized", nil) otherButtonTitles: nil];
[alert show];
[alert release];
[self downloadCompleted];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
{
// Concatenate the new data with the existing data to build up the downloaded file
// Update the status of the download
if (downloadCancelled)
{
[connection cancel];
[self downloadCompleted];
downloadCancelled = NO;
return;
}
[downloadedFileContents appendData:data];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
// downloadFileSize = [response expectedContentLength];
NSString * contentEncoding = [[(NSHTTPURLResponse *)response allHeaderFields] valueForKey:@"Content-Encoding"];
// NSDictionary *allHeaders = [(NSHTTPURLResponse *)response allHeaderFields];
isGzipCompressionUsedOnDownload = [[contentEncoding lowercaseString] isEqualToString:@"gzip"];
// for (id key in allHeaders)
// {
// NSLog(@"key: %@, value: %@", key, [allHeaders objectForKey:key]);
// }
//
// if (isGzipCompressionUsedOnDownload)
// NSLog(@"gzipping");
// Stop the spinning wheel and start the status bar for download
if ([response textEncodingName] != nil)
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedStringFromTable(@"Could not find file", @"Localized", nil) message:[NSString stringWithFormat:NSLocalizedStringFromTable(@"No such file exists on the server: %@", @"Localized", nil), nameOfDownloadedMolecule]
delegate:self cancelButtonTitle:NSLocalizedStringFromTable(@"OK", @"Localized", nil) otherButtonTitles: nil];
[alert show];
[alert release];
[connection cancel];
[self downloadCompleted];
return;
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
{
// UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Download completed" message:@"Download completed"
// delegate:self cancelButtonTitle:NSLocalizedStringFromTable(@"OK", @"Localized", nil) otherButtonTitles: nil];
// [alert show];
// [alert release];
// Close off the file and write it to disk
[self saveMoleculeWithData:downloadedFileContents toFilename:nameOfDownloadedMolecule];
[self downloadCompleted];
}
@end