// // Cache.m // Geopher Lite // // Created by Branden Russell on 8/16/08. // Copyright 2008 Stone Software and Branden Russell. All rights reserved. // #import "Cache.h" #import "Log.h" #import "TravelBug.h" // Static variables for compiled SQL queries. This implementation choice is to be able to share a one time // compilation of each query across all instances of the class. Each time a query is used, variables may be bound // to it, it will be "stepped", and then reset for the next usage. When the application begins to terminate, // a class method will be invoked to "finalize" (delete) the compiled queries - this must happen before the database // can be closed. static sqlite3_stmt *insert_statement = nil; static sqlite3_stmt *init_statement = nil; static sqlite3_stmt *delete_statement = nil; static sqlite3_stmt *hydrate_statement = nil; static sqlite3_stmt *dehydrate_statement = nil; @implementation Cache @synthesize logs; @synthesize travelBugs; // Finalize (delete) all of the SQLite compiled queries. + (void)finalizeStatements { if (insert_statement) sqlite3_finalize(insert_statement); if (init_statement) sqlite3_finalize(init_statement); if (delete_statement) sqlite3_finalize(delete_statement); if (hydrate_statement) sqlite3_finalize(hydrate_statement); if (dehydrate_statement) sqlite3_finalize(dehydrate_statement); } // Creates the object with primary key and title is brought into memory. - (id)initWithPrimaryKey:(NSInteger)pk database:(sqlite3 *)db { if ( (self = [super init] )) { primaryKey = pk; database = db; // Compile the query for retrieving cache data. See insertIntoDatabase: for more detail. if (init_statement == nil) { // Note the '?' at the end of the query. This is a parameter which can be replaced by a bound variable. // This is a great way to optimize because frequently used queries can be compiled once, then with each // use new variable values can be bound to placeholders. const char *sql = "SELECT name FROM cache WHERE pk=?"; if (sqlite3_prepare_v2(database, sql, -1, &init_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // For this query, we bind the primary key to the first (and only) placeholder in the statement. // Note that the parameters are numbered from 1, not from 0. sqlite3_bind_int(init_statement, 1, primaryKey); if (sqlite3_step(init_statement) == SQLITE_ROW) { self.name = [NSString stringWithUTF8String:(char *)sqlite3_column_text(init_statement, 0)]; } else { self.name = @"Unknown"; } // Reset the statement for future reuse. sqlite3_reset(init_statement); dirty = NO; } return self; } - (void)loadTravelBugsFromDatabase { NSMutableArray *array = [[NSMutableArray alloc] init]; self.travelBugs = array; [array release]; // Get the primary key for all logs from the table passed in as a parameter. strLoadTravelBugsFromDatabase = [NSString stringWithFormat:@"SELECT pk FROM %@", self.travelBugTblName]; const char *sql = [strLoadTravelBugsFromDatabase UTF8String]; sqlite3_stmt *statement; // Preparing a statement compiles the SQL query into a byte-code program in the SQLite library. // The third parameter is either the length of the SQL string or -1 to read up to the first null terminator. if (sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLITE_OK) { // We "step" through the results - once for each row. while (sqlite3_step(statement) == SQLITE_ROW) { // The second parameter indicates the column index into the result set. int pk = sqlite3_column_int(statement, 0); // We avoid the alloc-init-autorelease pattern here because we are in a tight loop and // autorelease is slightly more expensive than release. This design choice has nothing to do with // actual memory management - at the end of this block of code, all the cache objects allocated // here will be in memory regardless of whether we use autorelease or release, because they are // retained by the caches array. TravelBug *bug = [[TravelBug alloc] initWithPrimaryKey:pk database:database tableName:self.travelBugTblName]; [travelBugs addObject:bug]; [bug release]; } } // "Finalize" the statement - releases the resources associated with the statement. sqlite3_finalize(statement); } - (void)loadLogsFromDatabase { NSMutableArray *array = [[NSMutableArray alloc] init]; self.logs = array; [array release]; // Get the primary key for all logs from the table passed in as a parameter. strLoadLogsFromDatabase = [NSString stringWithFormat:@"SELECT pk FROM %@", self.logTblName]; const char *sql = [strLoadLogsFromDatabase UTF8String]; sqlite3_stmt *statement; // Preparing a statement compiles the SQL query into a byte-code program in the SQLite library. // The third parameter is either the length of the SQL string or -1 to read up to the first null terminator. if (sqlite3_prepare_v2(database, sql, -1, &statement, NULL) == SQLITE_OK) { // We "step" through the results - once for each row. while (sqlite3_step(statement) == SQLITE_ROW) { // The second parameter indicates the column index into the result set. int pk = sqlite3_column_int(statement, 0); // We avoid the alloc-init-autorelease pattern here because we are in a tight loop and // autorelease is slightly more expensive than release. This design choice has nothing to do with // actual memory management - at the end of this block of code, all the cache objects allocated // here will be in memory regardless of whether we use autorelease or release, because they are // retained by the caches array. Log *localLog = [[Log alloc] initWithPrimaryKey:pk database:database tableName:self.logTblName]; [logs addObject:localLog]; [localLog release]; } } // "Finalize" the statement - releases the resources associated with the statement. sqlite3_finalize(statement); } - (void)insertIntoDatabase:(sqlite3 *)db { database = db; // This query may be performed many times during the run of the application. As an optimization, a static // variable is used to store the SQLite compiled byte-code for the query, which is generated one time - the first // time the method is executed by any cache object. if (insert_statement == nil) { strInsertIntoDatabase = [NSString stringWithFormat:@"INSERT INTO %@ (name) VALUES(?)", self.logTblName]; const char *sql = [strInsertIntoDatabase UTF8String]; if (sqlite3_prepare_v2(database, sql, -1, &insert_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } sqlite3_bind_text(insert_statement, 1, [name UTF8String], -1, SQLITE_TRANSIENT); int success = sqlite3_step(insert_statement); // Because we want to reuse the statement, we "reset" it instead of "finalizing" it. sqlite3_reset(insert_statement); if (success == SQLITE_ERROR) { NSAssert1(0, @"Error: failed to insert into the database with message '%s'.", sqlite3_errmsg(database)); } else { // SQLite provides a method which retrieves the value of the most recently auto-generated primary key sequence // in the database. To access this functionality, the table should have a column declared of type // "INTEGER PRIMARY KEY" primaryKey = sqlite3_last_insert_rowid(database); } // All data for the cache is already in memory, but has not be written to the database // Mark as hydrated to prevent empty/default values from overwriting what is in memory hydrated = YES; } - (void)deleteFromDatabase { // Compile the delete statement if needed. if (delete_statement == nil) { const char *sql = "DELETE FROM cache WHERE pk=?"; if (sqlite3_prepare_v2(database, sql, -1, &delete_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // Bind the primary key variable. sqlite3_bind_int(delete_statement, 1, primaryKey); // Execute the query. int success = sqlite3_step(delete_statement); // Reset the statement for future use. sqlite3_reset(delete_statement); // Handle errors. if (success != SQLITE_DONE) { NSAssert1(0, @"Error: failed to delete from database with message '%s'.", sqlite3_errmsg(database)); } } // Brings the rest of the object data into memory. If already in memory, no action is taken (harmless no-op). - (void)hydrate { // Check if action is necessary. if (hydrated) return; // Compile the hydration statement, if needed. if (hydrate_statement == nil) { const char *sql = "SELECT wayPointCode, lat, lon, time, url, found, available, archived, placedBy, ownerID, type, containerSize, difficulty, terrain, country, state, shortDesc, longDesc, hints, logTblName, travelBugTblName, userMajorCategory, userMinorCategory FROM cache WHERE pk=?"; if (sqlite3_prepare_v2(database, sql, -1, &hydrate_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // Bind the primary key variable. sqlite3_bind_int(hydrate_statement, 1, primaryKey); // Execute the query. int success =sqlite3_step(hydrate_statement); if (success == SQLITE_ROW) { int idx = 0; char *str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.wayPointCode = (str) ? [NSString stringWithUTF8String:str] : @""; self.lat = sqlite3_column_double(hydrate_statement, idx++); self.lon = sqlite3_column_double(hydrate_statement, idx++); self.time = [NSDate dateWithTimeIntervalSince1970:sqlite3_column_double(hydrate_statement, idx++)]; str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.url = (str) ? [NSString stringWithUTF8String:str] : @""; self.found = sqlite3_column_int(hydrate_statement, idx++); self.available = sqlite3_column_int(hydrate_statement, idx++); self.archived = sqlite3_column_int(hydrate_statement, idx++); str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.placedBy = (str) ? [NSString stringWithUTF8String:str] : @""; self.ownerId = sqlite3_column_int(hydrate_statement, idx++); self.type = sqlite3_column_int(hydrate_statement, idx++); self.containerSize = sqlite3_column_int(hydrate_statement, idx++); self.difficulty = sqlite3_column_int(hydrate_statement, idx++); self.terrain = sqlite3_column_int(hydrate_statement, idx++); str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.country = (str) ? [NSString stringWithUTF8String:str] : @""; str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.state = (str) ? [NSString stringWithUTF8String:str] : @""; str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.shortDesc = (str) ? [NSString stringWithUTF8String:str] : @""; str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.longDesc = (str) ? [NSString stringWithUTF8String:str] : @""; str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.hints = (str) ? [NSString stringWithUTF8String:str] : @""; str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.logTblName = (str) ? [NSString stringWithUTF8String:str] : @""; str = nil; str = (char *)sqlite3_column_text(hydrate_statement, idx++); self.travelBugTblName = (str) ? [NSString stringWithUTF8String:str] : @""; self.userMajorCategory = sqlite3_column_int(hydrate_statement, idx++); self.userMinorCategory = sqlite3_column_int(hydrate_statement, idx++); } else { // The query did not return self.wayPointCode = @"None"; self.lat = 0; self.lon = 0; self.time = [NSDate distantPast]; self.url = @"None"; self.found = false; self.available = false; self.archived = false; self.placedBy = @"None"; self.ownerId = 0; self.type = 0; self.containerSize = 0; self.difficulty = 0; self.terrain = 0; self.country = @"None"; self.state = @"None"; self.shortDesc = @"None"; self.longDesc = @"None"; self.hints = @"None"; self.logTblName = @"None"; self.travelBugTblName = @"None"; self.userMajorCategory = 0; self.userMinorCategory = 0; } // Reset the query for the next use. sqlite3_reset(hydrate_statement); // Update object state with respect to hydrated = YES; } // Flushes all but the primary key and name out to the database. - (void)dehydrate { if (dirty) { // Write any changes to the database. // First, if needed, compile the dehydrate query. if (dehydrate_statement == nil) { const char *sql = "UPDATE cache SET name=?, wayPointCode=?, lat=?, lon=?, time=?, url=?, found=?, available=?, archived=?, placedBy=?, ownerID=?, type=?, containerSize=?, difficulty=?, terrain=?, country=?, state=?, shortDesc=?, longDesc=?, hints=?, logTblName=?, travelBugTblName=?, userMajorCategory=?, userMinorCategory=? WHERE pk=?"; if (sqlite3_prepare_v2(database, sql, -1, &dehydrate_statement, NULL) != SQLITE_OK) { NSAssert1(0, @"Error: failed to prepare statement with message '%s'.", sqlite3_errmsg(database)); } } // Bind the query variables. int idx = 0; sqlite3_bind_text(dehydrate_statement, idx++, [name UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_text(dehydrate_statement, idx++, [wayPointCode UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_double(dehydrate_statement, idx++, lat); sqlite3_bind_double(dehydrate_statement, idx++, lon); sqlite3_bind_double(dehydrate_statement, idx++, [time timeIntervalSince1970]); sqlite3_bind_text(dehydrate_statement, idx++, [url UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_int(dehydrate_statement, idx++, found); sqlite3_bind_int(dehydrate_statement, idx++, available); sqlite3_bind_int(dehydrate_statement, idx++, archived); sqlite3_bind_text(dehydrate_statement, idx++, [placedBy UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_int(dehydrate_statement, idx++, ownerId); sqlite3_bind_int(dehydrate_statement, idx++, type); sqlite3_bind_int(dehydrate_statement, idx++, containerSize); sqlite3_bind_int(dehydrate_statement, idx++, difficulty); sqlite3_bind_int(dehydrate_statement, idx++, terrain); sqlite3_bind_text(dehydrate_statement, idx++, [country UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_text(dehydrate_statement, idx++, [state UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_text(dehydrate_statement, idx++, [shortDesc UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_text(dehydrate_statement, idx++, [longDesc UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_text(dehydrate_statement, idx++, [hints UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_text(dehydrate_statement, idx++, [logTblName UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_text(dehydrate_statement, idx++, [travelBugTblName UTF8String], -1, SQLITE_TRANSIENT); sqlite3_bind_int(dehydrate_statement, idx++, userMajorCategory); sqlite3_bind_int(dehydrate_statement, idx++, userMinorCategory); sqlite3_bind_int(dehydrate_statement, idx++, primaryKey); // Execute the query. int success = sqlite3_step(dehydrate_statement); // Reset the query for the next use. sqlite3_reset(dehydrate_statement); // Handle errors. if (success != SQLITE_DONE) { NSAssert1(0, @"Error: failed to dehydrate with message '%s'.", sqlite3_errmsg(database)); } // Update the object state with respect to unwritten changes. dirty = NO; } // Release member variables to reclaim memory. Set to nil to avoid over-releasing them // if dehydrate is called multiple times. [logs makeObjectsPerformSelector:@selector(dehydrate)]; [travelBugs makeObjectsPerformSelector:@selector(dehydrate)]; [time release]; time = nil; [url release]; url = nil; [placedBy release]; placedBy = nil; [country release]; country = nil; [state release]; state = nil; [shortDesc release]; shortDesc = nil; [longDesc release]; longDesc = nil; [hints release]; hints = nil; [logTblName release]; logTblName = nil; [travelBugTblName release]; travelBugTblName = nil; // TODO: Dehydrate logs // TODO: Dehydrate travelBugs [data release]; data = nil; // Update the object state with respect to hydration. hydrated = NO; } - (void)dealloc { [name release]; [logTblName release]; [logs release]; [super dealloc]; } #pragma mark Properties // Accessors implemented below. All the "get" accessors simply return the value directly, with no additional // logic or steps for synchronization. The "set" accessors attempt to verify that the new value is definitely // different from the old value, to minimize the amount of work done. Any "set" which actually results in changing // data will mark the object as "dirty" - i.e., possessing data that has not been written to the database. // All the "set" accessors copy data, rather than retain it. This is common for value objects - strings, numbers, // dates, data buffers, etc. This ensures that subsequent changes to either the original or the copy don't violate // the encapsulation of the owning object. - (NSInteger)primaryKey { return primaryKey; } - (NSString *)name { return name; } - (void)setName:(NSString *)aString { if ((!name && !aString) || (name && aString && [name isEqualToString:aString])) return; dirty = YES; [name release]; name = [aString copy]; } - (NSString *)wayPointCode { return wayPointCode; } - (void)setWayPointCode:(NSString *)aString { if ((!wayPointCode && !aString) || (wayPointCode && aString && [wayPointCode isEqualToString:aString])) return; dirty = YES; [wayPointCode release]; wayPointCode = [aString copy]; } - (double)lat { return lat; } - (void)setLat:(double)val { if ((!lat && !val) || (lat && val && lat == val)) return; dirty = YES; lat = val; } - (double)lon { return lon; } - (void)setLon:(double)val { if ((!lon && !val) || (lon && val && lon == val)) return; dirty = YES; lon = val; } - (NSDate *)time { return time; } - (void)setTime:(NSDate *)val { if ((!time && !val) || (time && val && [time isEqualToDate:val])) return; dirty = YES; [time release]; time = [val copy]; } - (NSString *)url { return url; } - (void)setUrl:(NSString *)aString { if ((!url && !aString) || (url && aString && [url isEqualToString:aString])) return; dirty = YES; [url release]; url = [aString copy]; } - (bool)found { return found; } - (void)setFound:(bool)val { if ((!found && !val) || (found && val && found == val)) return; dirty = YES; found = val; } - (bool)available { return available; } - (void)setAvailable:(bool)val { if ((!available && !val) || (available && val && available == val)) return; dirty = YES; available = val; } - (bool)archived { return archived; } - (void)setArchived:(bool)val { if ((!archived && !val) || (archived && val && archived == val)) return; dirty = YES; archived = val; } - (NSString *)placedBy { return placedBy; } - (void)setPlacedBy:(NSString *)aString { if ((!placedBy && !aString) || (placedBy && aString && [placedBy isEqualToString:aString])) return; dirty = YES; [placedBy release]; placedBy = [aString copy]; } - (int)ownerId { return ownerId; } - (void)setOwnerId:(int)val { if ((!ownerId && !val) || (ownerId && val && ownerId == val)) return; dirty = YES; ownerId = val; } - (int)type { return type; } - (void)setType:(int)val { if ((!type && !val) || (type && val && type == val)) return; dirty = YES; type = val; } - (int)containerSize { return containerSize; } - (void)setContainerSize:(int)val { if ((!containerSize && !val) || (containerSize && val && containerSize == val)) return; dirty = YES; containerSize = val; } - (int)difficulty { return difficulty; } - (void)setDifficulty:(int)val { if ((!difficulty && !val) || (difficulty && val && difficulty == val)) return; dirty = YES; difficulty = val; } - (int)terrain { return terrain; } - (void)setTerrain:(int)val { if ((!terrain && !val) || (terrain && val && terrain == val)) return; dirty = YES; terrain = val; } - (NSString *)country { return country; } - (void)setCountry:(NSString *)aString { if ((!country && !aString) || (country && aString && [country isEqualToString:aString])) return; dirty = YES; [country release]; country = [aString copy]; } - (NSString *)state { return state; } - (void)setState:(NSString *)aString { if ((!state && !aString) || (state && aString && [state isEqualToString:aString])) return; dirty = YES; [state release]; state = [aString copy]; } - (NSString *)shortDesc { return shortDesc; } - (void)setShortDesc:(NSString *)aString { if ((!shortDesc && !aString) || (shortDesc && aString && [shortDesc isEqualToString:aString])) return; dirty = YES; [shortDesc release]; shortDesc = [aString copy]; } - (NSString *)longDesc { return longDesc; } - (void)setLongDesc:(NSString *)aString { if ((!longDesc && !aString) || (longDesc && aString && [longDesc isEqualToString:aString])) return; dirty = YES; [longDesc release]; longDesc = [aString copy]; } - (NSString *)hints { return hints; } - (void)setHints:(NSString *)aString { if ((!hints && !aString) || (hints && aString && [hints isEqualToString:aString])) return; dirty = YES; [hints release]; hints = [aString copy]; } - (NSString *)travelBugTblName { return travelBugTblName; } - (void)setTravelBugTblName:(NSString *)aString { if ((!travelBugTblName && !aString) || (travelBugTblName && aString && [travelBugTblName isEqualToString:aString])) return; dirty = YES; [travelBugTblName release]; travelBugTblName = [aString copy]; } -(NSMutableArray *)travelBugs { if( !travelBugs ) { [self loadTravelBugsFromDatabase]; } return travelBugs; } - (NSString *)logTblName { return logTblName; } - (void)setLogTblName:(NSString *)aString { if ((!logTblName && !aString) || (logTblName && aString && [logTblName isEqualToString:aString])) return; dirty = YES; [logTblName release]; logTblName = [aString copy]; } -(NSMutableArray *)logs { if( !logs ) { [self loadLogsFromDatabase]; } return logs; } - (int)userMajorCategory { return userMajorCategory; } - (void)setUserMajorCategory:(int)val { if ((!userMajorCategory && !val) || (userMajorCategory && val && userMajorCategory == val)) return; dirty = YES; userMajorCategory = val; } - (int)userMinorCategory { return userMinorCategory; } - (void)setUserMinorCategory:(int)val { if ((!userMinorCategory && !val) || (userMinorCategory && val && userMinorCategory == val)) return; dirty = YES; userMinorCategory = val; } @end