From 663d25e5160e97eefed1444bac0b4ac964f3368e Mon Sep 17 00:00:00 2001 From: matt handler Date: Sun, 24 Apr 2011 21:00:46 -0400 Subject: still a bit off from a solid state... added fmdatabase, updated acidcow parser with a formate, added some standard stuff to piccast app delegate, created a section dictionary, did some serializing to/from db for topics, added a bunch of logic to topic view controller, added db table --- Classes/AcidCowFeedburnerParser.m | 4 +- Classes/FMDatabase.h | 118 ++++++ Classes/FMDatabase.m | 815 ++++++++++++++++++++++++++++++++++++++ Classes/FMDatabaseAdditions.h | 31 ++ Classes/FMDatabaseAdditions.m | 114 ++++++ Classes/FMResultSet.h | 93 +++++ Classes/FMResultSet.m | 405 +++++++++++++++++++ Classes/PicCastAppDelegate.h | 2 + Classes/PicCastAppDelegate.m | 45 +++ Classes/SectionDictionary.h | 23 ++ Classes/SectionDictionary.m | 64 +++ Classes/Topic.h | 8 +- Classes/Topic.m | 58 ++- Classes/TopicsViewController.h | 7 +- Classes/TopicsViewController.m | 96 ++++- Classes/fmdb_Prefix.pch | 7 + 16 files changed, 1873 insertions(+), 17 deletions(-) create mode 100644 Classes/FMDatabase.h create mode 100644 Classes/FMDatabase.m create mode 100644 Classes/FMDatabaseAdditions.h create mode 100644 Classes/FMDatabaseAdditions.m create mode 100644 Classes/FMResultSet.h create mode 100644 Classes/FMResultSet.m create mode 100644 Classes/SectionDictionary.h create mode 100644 Classes/SectionDictionary.m create mode 100644 Classes/fmdb_Prefix.pch (limited to 'Classes') diff --git a/Classes/AcidCowFeedburnerParser.m b/Classes/AcidCowFeedburnerParser.m index 46d7ec5..118aa43 100644 --- a/Classes/AcidCowFeedburnerParser.m +++ b/Classes/AcidCowFeedburnerParser.m @@ -15,12 +15,12 @@ - (void)downloadAndParse:(NSURL *)url { - NSLog(@"downloadAndParse"); self.downloadAndParsePool = [[NSAutoreleasePool alloc] init]; done = NO; self.parseFormatter = [[[NSDateFormatter alloc] init] autorelease]; [parseFormatter setDateStyle:NSDateFormatterLongStyle]; [parseFormatter setTimeStyle:NSDateFormatterNoStyle]; + [parseFormatter setDateFormat:@"EEE, d MMM yyyy HH:mm:ss Z"]; // necessary because iTunes RSS feed is not localized, so if the device region has been set to other than US // the date formatter must be set to US locale in order to parse the dates [parseFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"US"] autorelease]]; @@ -169,7 +169,7 @@ static NSString *kName_Guid = @"guid"; } else if ([elementName isEqualToString:kName_Guid]) { currentTopic.guid = currentString; } else if ([elementName isEqualToString:kName_Creator]) { - currentTopic.creator = currentString; + //currentTopic.creator = currentString; } else if ([elementName isEqualToString:kName_Description]) { NSRegularExpression* regex = [NSRegularExpression regularExpressionWithPattern:@"http\\S+(jpg|png|gif|jpeg)" options:NSRegularExpressionCaseInsensitive diff --git a/Classes/FMDatabase.h b/Classes/FMDatabase.h new file mode 100644 index 0000000..f125d61 --- /dev/null +++ b/Classes/FMDatabase.h @@ -0,0 +1,118 @@ +#import +#import "sqlite3.h" +#import "FMResultSet.h" + +@interface FMDatabase : NSObject +{ + sqlite3* db; + NSString* databasePath; + BOOL logsErrors; + BOOL crashOnErrors; + BOOL inUse; + BOOL inTransaction; + BOOL traceExecution; + BOOL checkedOut; + int busyRetryTimeout; + BOOL shouldCacheStatements; + NSMutableDictionary *cachedStatements; + NSMutableSet *openResultSets; +} + + ++ (id)databaseWithPath:(NSString*)inPath; +- (id)initWithPath:(NSString*)inPath; + +- (BOOL)open; +#if SQLITE_VERSION_NUMBER >= 3005000 +- (BOOL)openWithFlags:(int)flags; +#endif +- (BOOL)close; +- (BOOL)goodConnection; +- (void)clearCachedStatements; +- (void)closeOpenResultSets; + +// encryption methods. You need to have purchased the sqlite encryption extensions for these to work. +- (BOOL)setKey:(NSString*)key; +- (BOOL)rekey:(NSString*)key; + + +- (NSString *)databasePath; + +- (NSString*)lastErrorMessage; + +- (int)lastErrorCode; +- (BOOL)hadError; +- (sqlite_int64)lastInsertRowId; + +- (sqlite3*)sqliteHandle; + +- (BOOL)update:(NSString*)sql error:(NSError**)outErr bind:(id)bindArgs, ...; +- (BOOL)executeUpdate:(NSString*)sql, ...; +- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments; +- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args; // you shouldn't ever need to call this. use the previous two instead. + +- (FMResultSet *)executeQuery:(NSString*)sql, ...; +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments; +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args; // you shouldn't ever need to call this. use the previous two instead. + +- (BOOL)rollback; +- (BOOL)commit; +- (BOOL)beginTransaction; +- (BOOL)beginDeferredTransaction; + +- (BOOL)logsErrors; +- (void)setLogsErrors:(BOOL)flag; + +- (BOOL)crashOnErrors; +- (void)setCrashOnErrors:(BOOL)flag; + +- (BOOL)inUse; +- (void)setInUse:(BOOL)value; + +- (BOOL)inTransaction; +- (void)setInTransaction:(BOOL)flag; + +- (BOOL)traceExecution; +- (void)setTraceExecution:(BOOL)flag; + +- (BOOL)checkedOut; +- (void)setCheckedOut:(BOOL)flag; + +- (int)busyRetryTimeout; +- (void)setBusyRetryTimeout:(int)newBusyRetryTimeout; + +- (BOOL)shouldCacheStatements; +- (void)setShouldCacheStatements:(BOOL)value; + +- (NSMutableDictionary *)cachedStatements; +- (void)setCachedStatements:(NSMutableDictionary *)value; + + ++ (NSString*)sqliteLibVersion; + +- (int)changes; + +@end + +@interface FMStatement : NSObject { + sqlite3_stmt *statement; + NSString *query; + long useCount; +} + + +- (void)close; +- (void)reset; + +- (sqlite3_stmt *)statement; +- (void)setStatement:(sqlite3_stmt *)value; + +- (NSString *)query; +- (void)setQuery:(NSString *)value; + +- (long)useCount; +- (void)setUseCount:(long)value; + + +@end + diff --git a/Classes/FMDatabase.m b/Classes/FMDatabase.m new file mode 100644 index 0000000..46d963b --- /dev/null +++ b/Classes/FMDatabase.m @@ -0,0 +1,815 @@ +#import "FMDatabase.h" +#import "unistd.h" + +@implementation FMDatabase + ++ (id)databaseWithPath:(NSString*)aPath { + return [[[self alloc] initWithPath:aPath] autorelease]; +} + +- (id)initWithPath:(NSString*)aPath { + self = [super init]; + + if (self) { + databasePath = [aPath copy]; + openResultSets = [[NSMutableSet alloc] init]; + db = 0x00; + logsErrors = 0x00; + crashOnErrors = 0x00; + busyRetryTimeout = 0x00; + } + + return self; +} + +- (void)finalize { + [self close]; + [super finalize]; +} + +- (void)dealloc { + [self close]; + + [openResultSets release]; + [cachedStatements release]; + [databasePath release]; + + [super dealloc]; +} + ++ (NSString*)sqliteLibVersion { + return [NSString stringWithFormat:@"%s", sqlite3_libversion()]; +} + +- (NSString *)databasePath { + return databasePath; +} + +- (sqlite3*)sqliteHandle { + return db; +} + +- (BOOL)open { + if (db) { + return YES; + } + + int err = sqlite3_open((databasePath ? [databasePath fileSystemRepresentation] : ":memory:"), &db ); + if(err != SQLITE_OK) { + NSLog(@"error opening!: %d", err); + return NO; + } + + return YES; +} + +#if SQLITE_VERSION_NUMBER >= 3005000 +- (BOOL)openWithFlags:(int)flags { + int err = sqlite3_open_v2((databasePath ? [databasePath fileSystemRepresentation] : ":memory:"), &db, flags, NULL /* Name of VFS module to use */); + if(err != SQLITE_OK) { + NSLog(@"error opening!: %d", err); + return NO; + } + return YES; +} +#endif + + +- (BOOL)close { + + [self clearCachedStatements]; + [self closeOpenResultSets]; + + if (!db) { + return YES; + } + + int rc; + BOOL retry; + int numberOfRetries = 0; + do { + retry = NO; + rc = sqlite3_close(db); + if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { + retry = YES; + usleep(20); + if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + NSLog(@"%s:%d", __FUNCTION__, __LINE__); + NSLog(@"Database busy, unable to close"); + return NO; + } + } + else if (SQLITE_OK != rc) { + NSLog(@"error closing!: %d", rc); + } + } + while (retry); + + db = nil; + return YES; +} + +- (void)clearCachedStatements { + + NSEnumerator *e = [cachedStatements objectEnumerator]; + FMStatement *cachedStmt; + + while ((cachedStmt = [e nextObject])) { + [cachedStmt close]; + } + + [cachedStatements removeAllObjects]; +} + +- (void)closeOpenResultSets { + //Copy the set so we don't get mutation errors + NSSet *resultSets = [[openResultSets copy] autorelease]; + + NSEnumerator *e = [resultSets objectEnumerator]; + NSValue *returnedResultSet = nil; + + while((returnedResultSet = [e nextObject])) { + FMResultSet *rs = (FMResultSet *)[returnedResultSet pointerValue]; + if ([rs respondsToSelector:@selector(close)]) { + [rs close]; + } + } +} + +- (void)resultSetDidClose:(FMResultSet *)resultSet { + NSValue *setValue = [NSValue valueWithNonretainedObject:resultSet]; + [openResultSets removeObject:setValue]; +} + +- (FMStatement*)cachedStatementForQuery:(NSString*)query { + return [cachedStatements objectForKey:query]; +} + +- (void)setCachedStatement:(FMStatement*)statement forQuery:(NSString*)query { + //NSLog(@"setting query: %@", query); + query = [query copy]; // in case we got handed in a mutable string... + [statement setQuery:query]; + [cachedStatements setObject:statement forKey:query]; + [query release]; +} + + +- (BOOL)rekey:(NSString*)key { +#ifdef SQLITE_HAS_CODEC + if (!key) { + return NO; + } + + int rc = sqlite3_rekey(db, [key UTF8String], strlen([key UTF8String])); + + if (rc != SQLITE_OK) { + NSLog(@"error on rekey: %d", rc); + NSLog(@"%@", [self lastErrorMessage]); + } + + return (rc == SQLITE_OK); +#else + return NO; +#endif +} + +- (BOOL)setKey:(NSString*)key { +#ifdef SQLITE_HAS_CODEC + if (!key) { + return NO; + } + + int rc = sqlite3_key(db, [key UTF8String], strlen([key UTF8String])); + + return (rc == SQLITE_OK); +#else + return NO; +#endif +} + +- (BOOL)goodConnection { + + if (!db) { + return NO; + } + + FMResultSet *rs = [self executeQuery:@"select name from sqlite_master where type='table'"]; + + if (rs) { + [rs close]; + return YES; + } + + return NO; +} + +- (void)compainAboutInUse { + NSLog(@"The FMDatabase %@ is currently in use.", self); + +#ifndef NS_BLOCK_ASSERTIONS + if (crashOnErrors) { + NSAssert1(false, @"The FMDatabase %@ is currently in use.", self); + } +#endif +} + +- (NSString*)lastErrorMessage { + return [NSString stringWithUTF8String:sqlite3_errmsg(db)]; +} + +- (BOOL)hadError { + int lastErrCode = [self lastErrorCode]; + + return (lastErrCode > SQLITE_OK && lastErrCode < SQLITE_ROW); +} + +- (int)lastErrorCode { + return sqlite3_errcode(db); +} + +- (sqlite_int64)lastInsertRowId { + + if (inUse) { + [self compainAboutInUse]; + return NO; + } + [self setInUse:YES]; + + sqlite_int64 ret = sqlite3_last_insert_rowid(db); + + [self setInUse:NO]; + + return ret; +} + +- (int)changes { + if (inUse) { + [self compainAboutInUse]; + return 0; + } + + [self setInUse:YES]; + int ret = sqlite3_changes(db); + [self setInUse:NO]; + + return ret; +} + +- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt { + + if ((!obj) || ((NSNull *)obj == [NSNull null])) { + sqlite3_bind_null(pStmt, idx); + } + + // FIXME - someday check the return codes on these binds. + else if ([obj isKindOfClass:[NSData class]]) { + sqlite3_bind_blob(pStmt, idx, [obj bytes], (int)[obj length], SQLITE_STATIC); + } + else if ([obj isKindOfClass:[NSDate class]]) { + sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]); + } + else if ([obj isKindOfClass:[NSNumber class]]) { + + if (strcmp([obj objCType], @encode(BOOL)) == 0) { + sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0)); + } + else if (strcmp([obj objCType], @encode(int)) == 0) { + sqlite3_bind_int64(pStmt, idx, [obj longValue]); + } + else if (strcmp([obj objCType], @encode(long)) == 0) { + sqlite3_bind_int64(pStmt, idx, [obj longValue]); + } + else if (strcmp([obj objCType], @encode(long long)) == 0) { + sqlite3_bind_int64(pStmt, idx, [obj longLongValue]); + } + else if (strcmp([obj objCType], @encode(float)) == 0) { + sqlite3_bind_double(pStmt, idx, [obj floatValue]); + } + else if (strcmp([obj objCType], @encode(double)) == 0) { + sqlite3_bind_double(pStmt, idx, [obj doubleValue]); + } + else { + sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC); + } + } + else { + sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC); + } +} + +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args { + + if (inUse) { + [self compainAboutInUse]; + return nil; + } + + [self setInUse:YES]; + + FMResultSet *rs = nil; + + int rc = 0x00;; + sqlite3_stmt *pStmt = 0x00;; + FMStatement *statement = 0x00; + + if (traceExecution && sql) { + NSLog(@"%@ executeQuery: %@", self, sql); + } + + if (shouldCacheStatements) { + statement = [self cachedStatementForQuery:sql]; + pStmt = statement ? [statement statement] : 0x00; + } + + int numberOfRetries = 0; + BOOL retry = NO; + + if (!pStmt) { + do { + retry = NO; + rc = sqlite3_prepare_v2(db, [sql UTF8String], -1, &pStmt, 0); + + if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { + retry = YES; + usleep(20); + + if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); + NSLog(@"Database busy"); + sqlite3_finalize(pStmt); + [self setInUse:NO]; + return nil; + } + } + else if (SQLITE_OK != rc) { + + + if (logsErrors) { + NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); + NSLog(@"DB Query: %@", sql); +#ifndef NS_BLOCK_ASSERTIONS + if (crashOnErrors) { + NSAssert2(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); + } +#endif + } + + sqlite3_finalize(pStmt); + + [self setInUse:NO]; + return nil; + } + } + while (retry); + } + + id obj; + int idx = 0; + int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!) + + while (idx < queryCount) { + + if (arrayArgs) { + obj = [arrayArgs objectAtIndex:idx]; + } + else { + obj = va_arg(args, id); + } + + if (traceExecution) { + NSLog(@"obj: %@", obj); + } + + idx++; + + [self bindObject:obj toColumn:idx inStatement:pStmt]; + } + + if (idx != queryCount) { + NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)"); + sqlite3_finalize(pStmt); + [self setInUse:NO]; + return nil; + } + + [statement retain]; // to balance the release below + + if (!statement) { + statement = [[FMStatement alloc] init]; + [statement setStatement:pStmt]; + + if (shouldCacheStatements) { + [self setCachedStatement:statement forQuery:sql]; + } + } + + // the statement gets closed in rs's dealloc or [rs close]; + rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self]; + [rs setQuery:sql]; + NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs]; + [openResultSets addObject:openResultSet]; + + statement.useCount = statement.useCount + 1; + + [statement release]; + + [self setInUse:NO]; + + return rs; +} + +- (FMResultSet *)executeQuery:(NSString*)sql, ... { + va_list args; + va_start(args, sql); + + id result = [self executeQuery:sql withArgumentsInArray:nil orVAList:args]; + + va_end(args); + return result; +} + +- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments { + return [self executeQuery:sql withArgumentsInArray:arguments orVAList:nil]; +} + +- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args { + + if (inUse) { + [self compainAboutInUse]; + return NO; + } + + [self setInUse:YES]; + + int rc = 0x00; + sqlite3_stmt *pStmt = 0x00; + FMStatement *cachedStmt = 0x00; + + if (traceExecution && sql) { + NSLog(@"%@ executeUpdate: %@", self, sql); + } + + if (shouldCacheStatements) { + cachedStmt = [self cachedStatementForQuery:sql]; + pStmt = cachedStmt ? [cachedStmt statement] : 0x00; + } + + int numberOfRetries = 0; + BOOL retry = NO; + + if (!pStmt) { + + do { + retry = NO; + rc = sqlite3_prepare_v2(db, [sql UTF8String], -1, &pStmt, 0); + if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { + retry = YES; + usleep(20); + + if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); + NSLog(@"Database busy"); + sqlite3_finalize(pStmt); + [self setInUse:NO]; + return NO; + } + } + else if (SQLITE_OK != rc) { + + + if (logsErrors) { + NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); + NSLog(@"DB Query: %@", sql); +#ifndef NS_BLOCK_ASSERTIONS + if (crashOnErrors) { + NSAssert2(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); + } +#endif + } + + sqlite3_finalize(pStmt); + [self setInUse:NO]; + + if (outErr) { + *outErr = [NSError errorWithDomain:[NSString stringWithUTF8String:sqlite3_errmsg(db)] code:rc userInfo:nil]; + } + + return NO; + } + } + while (retry); + } + + + id obj; + int idx = 0; + int queryCount = sqlite3_bind_parameter_count(pStmt); + + while (idx < queryCount) { + + if (arrayArgs) { + obj = [arrayArgs objectAtIndex:idx]; + } + else { + obj = va_arg(args, id); + } + + + if (traceExecution) { + NSLog(@"obj: %@", obj); + } + + idx++; + + [self bindObject:obj toColumn:idx inStatement:pStmt]; + } + + if (idx != queryCount) { + NSLog(@"Error: the bind count is not correct for the # of variables (%@) (executeUpdate)", sql); + sqlite3_finalize(pStmt); + [self setInUse:NO]; + return NO; + } + + /* Call sqlite3_step() to run the virtual machine. Since the SQL being + ** executed is not a SELECT statement, we assume no data will be returned. + */ + numberOfRetries = 0; + do { + rc = sqlite3_step(pStmt); + retry = NO; + + if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { + // this will happen if the db is locked, like if we are doing an update or insert. + // in that case, retry the step... and maybe wait just 10 milliseconds. + retry = YES; + if (SQLITE_LOCKED == rc) { + rc = sqlite3_reset(pStmt); + if (rc != SQLITE_LOCKED) { + NSLog(@"Unexpected result from sqlite3_reset (%d) eu", rc); + } + } + usleep(20); + + if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) { + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]); + NSLog(@"Database busy"); + retry = NO; + } + } + else if (SQLITE_DONE == rc || SQLITE_ROW == rc) { + // all is well, let's return. + } + else if (SQLITE_ERROR == rc) { + NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_ERROR", rc, sqlite3_errmsg(db)); + NSLog(@"DB Query: %@", sql); + } + else if (SQLITE_MISUSE == rc) { + // uh oh. + NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_MISUSE", rc, sqlite3_errmsg(db)); + NSLog(@"DB Query: %@", sql); + } + else { + // wtf? + NSLog(@"Unknown error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(db)); + NSLog(@"DB Query: %@", sql); + } + + } while (retry); + + assert( rc!=SQLITE_ROW ); + + + if (shouldCacheStatements && !cachedStmt) { + cachedStmt = [[FMStatement alloc] init]; + + [cachedStmt setStatement:pStmt]; + + [self setCachedStatement:cachedStmt forQuery:sql]; + + [cachedStmt release]; + } + + if (cachedStmt) { + cachedStmt.useCount = cachedStmt.useCount + 1; + rc = sqlite3_reset(pStmt); + } + else { + /* Finalize the virtual machine. This releases all memory and other + ** resources allocated by the sqlite3_prepare() call above. + */ + rc = sqlite3_finalize(pStmt); + } + + [self setInUse:NO]; + + return (rc == SQLITE_OK); +} + + +- (BOOL)executeUpdate:(NSString*)sql, ... { + va_list args; + va_start(args, sql); + + BOOL result = [self executeUpdate:sql error:nil withArgumentsInArray:nil orVAList:args]; + + va_end(args); + return result; +} + + + +- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments { + return [self executeUpdate:sql error:nil withArgumentsInArray:arguments orVAList:nil]; +} + +- (BOOL)update:(NSString*)sql error:(NSError**)outErr bind:(id)bindArgs, ... { + va_list args; + va_start(args, bindArgs); + + BOOL result = [self executeUpdate:sql error:outErr withArgumentsInArray:nil orVAList:args]; + + va_end(args); + return result; +} + +- (BOOL)rollback { + BOOL b = [self executeUpdate:@"ROLLBACK TRANSACTION;"]; + if (b) { + inTransaction = NO; + } + return b; +} + +- (BOOL)commit { + BOOL b = [self executeUpdate:@"COMMIT TRANSACTION;"]; + if (b) { + inTransaction = NO; + } + return b; +} + +- (BOOL)beginDeferredTransaction { + BOOL b = [self executeUpdate:@"BEGIN DEFERRED TRANSACTION;"]; + if (b) { + inTransaction = YES; + } + return b; +} + +- (BOOL)beginTransaction { + BOOL b = [self executeUpdate:@"BEGIN EXCLUSIVE TRANSACTION;"]; + if (b) { + inTransaction = YES; + } + return b; +} + +- (BOOL)logsErrors { + return logsErrors; +} +- (void)setLogsErrors:(BOOL)flag { + logsErrors = flag; +} + +- (BOOL)crashOnErrors { + return crashOnErrors; +} +- (void)setCrashOnErrors:(BOOL)flag { + crashOnErrors = flag; +} + +- (BOOL)inUse { + return inUse || inTransaction; +} + +- (void)setInUse:(BOOL)b { + inUse = b; +} + +- (BOOL)inTransaction { + return inTransaction; +} +- (void)setInTransaction:(BOOL)flag { + inTransaction = flag; +} + +- (BOOL)traceExecution { + return traceExecution; +} +- (void)setTraceExecution:(BOOL)flag { + traceExecution = flag; +} + +- (BOOL)checkedOut { + return checkedOut; +} +- (void)setCheckedOut:(BOOL)flag { + checkedOut = flag; +} + + +- (int)busyRetryTimeout { + return busyRetryTimeout; +} +- (void)setBusyRetryTimeout:(int)newBusyRetryTimeout { + busyRetryTimeout = newBusyRetryTimeout; +} + + +- (BOOL)shouldCacheStatements { + return shouldCacheStatements; +} + +- (void)setShouldCacheStatements:(BOOL)value { + + shouldCacheStatements = value; + + if (shouldCacheStatements && !cachedStatements) { + [self setCachedStatements:[NSMutableDictionary dictionary]]; + } + + if (!shouldCacheStatements) { + [self setCachedStatements:nil]; + } +} + +- (NSMutableDictionary *)cachedStatements { + return cachedStatements; +} + +- (void)setCachedStatements:(NSMutableDictionary *)value { + if (cachedStatements != value) { + [cachedStatements release]; + cachedStatements = [value retain]; + } +} + + +@end + + + +@implementation FMStatement + +- (void)finalize { + [self close]; + [super finalize]; +} + +- (void)dealloc { + [self close]; + [query release]; + [super dealloc]; +} + + +- (void)close { + if (statement) { + sqlite3_finalize(statement); + statement = 0x00; + } +} + +- (void)reset { + if (statement) { + sqlite3_reset(statement); + } +} + +- (sqlite3_stmt *)statement { + return statement; +} + +- (void)setStatement:(sqlite3_stmt *)value { + statement = value; +} + +- (NSString *)query { + return query; +} + +- (void)setQuery:(NSString *)value { + if (query != value) { + [query release]; + query = [value retain]; + } +} + +- (long)useCount { + return useCount; +} + +- (void)setUseCount:(long)value { + if (useCount != value) { + useCount = value; + } +} + +- (NSString*)description { + return [NSString stringWithFormat:@"%@ %d hit(s) for query %@", [super description], useCount, query]; +} + + +@end + diff --git a/Classes/FMDatabaseAdditions.h b/Classes/FMDatabaseAdditions.h new file mode 100644 index 0000000..3846268 --- /dev/null +++ b/Classes/FMDatabaseAdditions.h @@ -0,0 +1,31 @@ +// +// FMDatabaseAdditions.h +// fmkit +// +// Created by August Mueller on 10/30/05. +// Copyright 2005 Flying Meat Inc.. All rights reserved. +// + +#import +@interface FMDatabase (FMDatabaseAdditions) + + +- (int)intForQuery:(NSString*)objs, ...; +- (long)longForQuery:(NSString*)objs, ...; +- (BOOL)boolForQuery:(NSString*)objs, ...; +- (double)doubleForQuery:(NSString*)objs, ...; +- (NSString*)stringForQuery:(NSString*)objs, ...; +- (NSData*)dataForQuery:(NSString*)objs, ...; +- (NSDate*)dateForQuery:(NSString*)objs, ...; + +// Notice that there's no dataNoCopyForQuery:. +// That would be a bad idea, because we close out the result set, and then what +// happens to the data that we just didn't copy? Who knows, not I. + + +- (BOOL)tableExists:(NSString*)tableName; +- (FMResultSet*)getSchema; +- (FMResultSet*)getTableSchema:(NSString*)tableName; +- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName; + +@end diff --git a/Classes/FMDatabaseAdditions.m b/Classes/FMDatabaseAdditions.m new file mode 100644 index 0000000..3ac45cb --- /dev/null +++ b/Classes/FMDatabaseAdditions.m @@ -0,0 +1,114 @@ +// +// FMDatabaseAdditions.m +// fmkit +// +// Created by August Mueller on 10/30/05. +// Copyright 2005 Flying Meat Inc.. All rights reserved. +// + +#import "FMDatabase.h" +#import "FMDatabaseAdditions.h" + +@implementation FMDatabase (FMDatabaseAdditions) + +#define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \ +va_list args; \ +va_start(args, query); \ +FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orVAList:args]; \ +va_end(args); \ +if (![resultSet next]) { return (type)0; } \ +type ret = [resultSet sel:0]; \ +[resultSet close]; \ +[resultSet setParentDB:nil]; \ +return ret; + + +- (NSString*)stringForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSString *, stringForColumnIndex); +} + +- (int)intForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(int, intForColumnIndex); +} + +- (long)longForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(long, longForColumnIndex); +} + +- (BOOL)boolForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(BOOL, boolForColumnIndex); +} + +- (double)doubleForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(double, doubleForColumnIndex); +} + +- (NSData*)dataForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSData *, dataForColumnIndex); +} + +- (NSDate*)dateForQuery:(NSString*)query, ... { + RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSDate *, dateForColumnIndex); +} + + +//check if table exist in database (patch from OZLB) +- (BOOL)tableExists:(NSString*)tableName { + + BOOL returnBool; + //lower case table name + tableName = [tableName lowercaseString]; + //search in sqlite_master table if table exists + FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName]; + //if at least one next exists, table exists + returnBool = [rs next]; + //close and free object + [rs close]; + + return returnBool; +} + +//get table with list of tables: result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING] +//check if table exist in database (patch from OZLB) +- (FMResultSet*)getSchema { + + //result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING] + FMResultSet *rs = [self executeQuery:@"SELECT type, name, tbl_name, rootpage, sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type != 'meta' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, type DESC, name"]; + + return rs; +} + +//get table schema: result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER] +- (FMResultSet*)getTableSchema:(NSString*)tableName { + + //result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER] + FMResultSet *rs = [self executeQuery:[NSString stringWithFormat: @"PRAGMA table_info(%@)", tableName]]; + + return rs; +} + + +//check if column exist in table +- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName { + + BOOL returnBool = NO; + //lower case table name + tableName = [tableName lowercaseString]; + //lower case column name + columnName = [columnName lowercaseString]; + //get table schema + FMResultSet *rs = [self getTableSchema: tableName]; + //check if column is present in table schema + while ([rs next]) { + if ([[[rs stringForColumn:@"name"] lowercaseString] isEqualToString: columnName]) { + returnBool = YES; + break; + } + } + //close and free object + [rs close]; + + return returnBool; +} + +@end diff --git a/Classes/FMResultSet.h b/Classes/FMResultSet.h new file mode 100644 index 0000000..306676e --- /dev/null +++ b/Classes/FMResultSet.h @@ -0,0 +1,93 @@ +#import +#import "sqlite3.h" + +#ifndef __has_feature // Optional. +#define __has_feature(x) 0 // Compatibility with non-clang compilers. +#endif + +#ifndef NS_RETURNS_NOT_RETAINED +#if __has_feature(attribute_ns_returns_not_retained) +#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained)) +#else +#define NS_RETURNS_NOT_RETAINED +#endif +#endif + +@class FMDatabase; +@class FMStatement; + +@interface FMResultSet : NSObject { + FMDatabase *parentDB; + FMStatement *statement; + + NSString *query; + NSMutableDictionary *columnNameToIndexMap; + BOOL columnNamesSetup; +} + + ++ (id)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB; + +- (void)close; + +- (NSString *)query; +- (void)setQuery:(NSString *)value; + +- (FMStatement *)statement; +- (void)setStatement:(FMStatement *)value; + +- (void)setParentDB:(FMDatabase *)newDb; + +- (BOOL)next; +- (BOOL)hasAnotherRow; + +- (int)columnIndexForName:(NSString*)columnName; +- (NSString*)columnNameForIndex:(int)columnIdx; + +- (int)intForColumn:(NSString*)columnName; +- (int)intForColumnIndex:(int)columnIdx; + +- (long)longForColumn:(NSString*)columnName; +- (long)longForColumnIndex:(int)columnIdx; + +- (long long int)longLongIntForColumn:(NSString*)columnName; +- (long long int)longLongIntForColumnIndex:(int)columnIdx; + +- (BOOL)boolForColumn:(NSString*)columnName; +- (BOOL)boolForColumnIndex:(int)columnIdx; + +- (double)doubleForColumn:(NSString*)columnName; +- (double)doubleForColumnIndex:(int)columnIdx; + +- (NSString*)stringForColumn:(NSString*)columnName; +- (NSString*)stringForColumnIndex:(int)columnIdx; + +- (NSDate*)dateForColumn:(NSString*)columnName; +- (NSDate*)dateForColumnIndex:(int)columnIdx; + +- (NSData*)dataForColumn:(NSString*)columnName; +- (NSData*)dataForColumnIndex:(int)columnIdx; + +- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx; +- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName; + +// returns one of NSNumber, NSString, NSData, or NSNull +- (id)objectForColumnIndex:(int)columnIdx; +- (id)objectForColumnName:(NSString*)columnName; + +/* +If you are going to use this data after you iterate over the next row, or after you close the +result set, make sure to make a copy of the data first (or just use dataForColumn:/dataForColumnIndex:) +If you don't, you're going to be in a world of hurt when you try and use the data. +*/ +- (NSData*)dataNoCopyForColumn:(NSString*)columnName NS_RETURNS_NOT_RETAINED; +- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED; + + +- (BOOL)columnIndexIsNull:(int)columnIdx; +- (BOOL)columnIsNull:(NSString*)columnName; + +- (void)kvcMagic:(id)object; +- (NSDictionary *)resultDict; + +@end diff --git a/Classes/FMResultSet.m b/Classes/FMResultSet.m new file mode 100644 index 0000000..027257e --- /dev/null +++ b/Classes/FMResultSet.m @@ -0,0 +1,405 @@ +#import "FMResultSet.h" +#import "FMDatabase.h" +#import "unistd.h" + +@interface FMDatabase () +- (void)resultSetDidClose:(FMResultSet *)resultSet; +@end + + +@interface FMResultSet (Private) +- (NSMutableDictionary *)columnNameToIndexMap; +- (void)setColumnNameToIndexMap:(NSMutableDictionary *)value; +@end + +@implementation FMResultSet + ++ (id)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB { + + FMResultSet *rs = [[FMResultSet alloc] init]; + + [rs setStatement:statement]; + [rs setParentDB:aDB]; + + return [rs autorelease]; +} + +- (void)finalize { + [self close]; + [super finalize]; +} + +- (void)dealloc { + [self close]; + + [query release]; + query = nil; + + [columnNameToIndexMap release]; + columnNameToIndexMap = nil; + + [super dealloc]; +} + +- (void)close { + [statement reset]; + [statement release]; + statement = nil; + + // we don't need this anymore... (i think) + //[parentDB setInUse:NO]; + [parentDB resultSetDidClose:self]; + parentDB = nil; +} + +- (void)setupColumnNames { + + if (!columnNameToIndexMap) { + [self setColumnNameToIndexMap:[NSMutableDictionary dictionary]]; + } + + int columnCount = sqlite3_column_count(statement.statement); + + int columnIdx = 0; + for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { + [columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx] + forKey:[[NSString stringWithUTF8String:sqlite3_column_name(statement.statement, columnIdx)] lowercaseString]]; + } + columnNamesSetup = YES; +} + +- (void)kvcMagic:(id)object { + + int columnCount = sqlite3_column_count(statement.statement); + + int columnIdx = 0; + for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { + + const char *c = (const char *)sqlite3_column_text(statement.statement, columnIdx); + + // check for a null row + if (c) { + NSString *s = [NSString stringWithUTF8String:c]; + + [object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name(statement.statement, columnIdx)]]; + } + } +} + +- (NSDictionary *)resultDict { + + int num_cols = sqlite3_data_count(statement.statement); + + if (num_cols > 0) { + NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols]; + + if (!columnNamesSetup) { + [self setupColumnNames]; + } + + NSEnumerator *columnNames = [columnNameToIndexMap keyEnumerator]; + NSString *columnName = nil; + while ((columnName = [columnNames nextObject])) { + id objectValue = [self objectForColumnName:columnName]; + [dict setObject:objectValue forKey:columnName]; + } + + return [[dict copy] autorelease]; + } + else { + NSLog(@"Warning: There seem to be no columns in this set."); + } + + return nil; +} + +- (BOOL)next { + + int rc; + BOOL retry; + int numberOfRetries = 0; + do { + retry = NO; + + rc = sqlite3_step(statement.statement); + + if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { + // this will happen if the db is locked, like if we are doing an update or insert. + // in that case, retry the step... and maybe wait just 10 milliseconds. + retry = YES; + if (SQLITE_LOCKED == rc) { + rc = sqlite3_reset(statement.statement); + if (rc != SQLITE_LOCKED) { + NSLog(@"Unexpected result from sqlite3_reset (%d) rs", rc); + } + } + usleep(20); + + if ([parentDB busyRetryTimeout] && (numberOfRetries++ > [parentDB busyRetryTimeout])) { + + NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [parentDB databasePath]); + NSLog(@"Database busy"); + break; + } + } + else if (SQLITE_DONE == rc || SQLITE_ROW == rc) { + // all is well, let's return. + } + else if (SQLITE_ERROR == rc) { + NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([parentDB sqliteHandle])); + break; + } + else if (SQLITE_MISUSE == rc) { + // uh oh. + NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([parentDB sqliteHandle])); + break; + } + else { + // wtf? + NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([parentDB sqliteHandle])); + break; + } + + } while (retry); + + + if (rc != SQLITE_ROW) { + [self close]; + } + + return (rc == SQLITE_ROW); +} + +- (BOOL)hasAnotherRow { + return sqlite3_errcode([parentDB sqliteHandle]) == SQLITE_ROW; +} + +- (int)columnIndexForName:(NSString*)columnName { + + if (!columnNamesSetup) { + [self setupColumnNames]; + } + + columnName = [columnName lowercaseString]; + + NSNumber *n = [columnNameToIndexMap objectForKey:columnName]; + + if (n) { + return [n intValue]; + } + + NSLog(@"Warning: I could not find the column named '%@'.", columnName); + + return -1; +} + + + +- (int)intForColumn:(NSString*)columnName { + return [self intForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (int)intForColumnIndex:(int)columnIdx { + return sqlite3_column_int(statement.statement, columnIdx); +} + +- (long)longForColumn:(NSString*)columnName { + return [self longForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (long)longForColumnIndex:(int)columnIdx { + return (long)sqlite3_column_int64(statement.statement, columnIdx); +} + +- (long long int)longLongIntForColumn:(NSString*)columnName { + return [self longLongIntForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (long long int)longLongIntForColumnIndex:(int)columnIdx { + return sqlite3_column_int64(statement.statement, columnIdx); +} + +- (BOOL)boolForColumn:(NSString*)columnName { + return [self boolForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (BOOL)boolForColumnIndex:(int)columnIdx { + return ([self intForColumnIndex:columnIdx] != 0); +} + +- (double)doubleForColumn:(NSString*)columnName { + return [self doubleForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (double)doubleForColumnIndex:(int)columnIdx { + return sqlite3_column_double(statement.statement, columnIdx); +} + +- (NSString*)stringForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + const char *c = (const char *)sqlite3_column_text(statement.statement, columnIdx); + + if (!c) { + // null row. + return nil; + } + + return [NSString stringWithUTF8String:c]; +} + +- (NSString*)stringForColumn:(NSString*)columnName { + return [self stringForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSDate*)dateForColumn:(NSString*)columnName { + return [self dateForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSDate*)dateForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + return [NSDate dateWithTimeIntervalSince1970:[self doubleForColumnIndex:columnIdx]]; +} + + +- (NSData*)dataForColumn:(NSString*)columnName { + return [self dataForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSData*)dataForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + int dataSize = sqlite3_column_bytes(statement.statement, columnIdx); + + NSMutableData *data = [NSMutableData dataWithLength:dataSize]; + + memcpy([data mutableBytes], sqlite3_column_blob(statement.statement, columnIdx), dataSize); + + return data; +} + + +- (NSData*)dataNoCopyForColumn:(NSString*)columnName { + return [self dataNoCopyForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + int dataSize = sqlite3_column_bytes(statement.statement, columnIdx); + + NSData *data = [NSData dataWithBytesNoCopy:(void *)sqlite3_column_blob(statement.statement, columnIdx) length:dataSize freeWhenDone:NO]; + + return data; +} + + +- (BOOL)columnIndexIsNull:(int)columnIdx { + return sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL; +} + +- (BOOL)columnIsNull:(NSString*)columnName { + return [self columnIndexIsNull:[self columnIndexForName:columnName]]; +} + +- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx { + + if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx < 0)) { + return nil; + } + + return sqlite3_column_text(statement.statement, columnIdx); +} + +- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName { + return [self UTF8StringForColumnIndex:[self columnIndexForName:columnName]]; +} + +- (id)objectForColumnIndex:(int)columnIdx { + int columnType = sqlite3_column_type(statement.statement, columnIdx); + + id returnValue = nil; + + if (columnType == SQLITE_INTEGER) { + returnValue = [NSNumber numberWithLongLong:[self longLongIntForColumnIndex:columnIdx]]; + } + else if (columnType == SQLITE_FLOAT) { + returnValue = [NSNumber numberWithDouble:[self doubleForColumnIndex:columnIdx]]; + } + else if (columnType == SQLITE_BLOB) { + returnValue = [self dataForColumnIndex:columnIdx]; + } + else { + //default to a string for everything else + returnValue = [self stringForColumnIndex:columnIdx]; + } + + if (returnValue == nil) { + returnValue = [NSNull null]; + } + + return returnValue; +} + +- (id)objectForColumnName:(NSString*)columnName { + return [self objectForColumnIndex:[self columnIndexForName:columnName]]; +} + + +// returns autoreleased NSString containing the name of the column in the result set +- (NSString*)columnNameForIndex:(int)columnIdx { + return [NSString stringWithUTF8String: sqlite3_column_name(statement.statement, columnIdx)]; +} + +- (void)setParentDB:(FMDatabase *)newDb { + parentDB = newDb; +} + + +- (NSString *)query { + return query; +} + +- (void)setQuery:(NSString *)value { + [value retain]; + [query release]; + query = value; +} + +- (NSMutableDictionary *)columnNameToIndexMap { + return columnNameToIndexMap; +} + +- (void)setColumnNameToIndexMap:(NSMutableDictionary *)value { + [value retain]; + [columnNameToIndexMap release]; + columnNameToIndexMap = value; +} + +- (FMStatement *)statement { + return statement; +} + +- (void)setStatement:(FMStatement *)value { + if (statement != value) { + [statement release]; + statement = [value retain]; + } +} + + + +@end diff --git a/Classes/PicCastAppDelegate.h b/Classes/PicCastAppDelegate.h index a0efe4d..1085903 100644 --- a/Classes/PicCastAppDelegate.h +++ b/Classes/PicCastAppDelegate.h @@ -18,4 +18,6 @@ @property (nonatomic, retain) IBOutlet UITabBarController *tabBarController; @property (nonatomic, retain) IBOutlet UINavigationController *navigationController; ++ (NSString *) getDatabasePath; + @end diff --git a/Classes/PicCastAppDelegate.m b/Classes/PicCastAppDelegate.m index ab242ec..f44c6eb 100644 --- a/Classes/PicCastAppDelegate.m +++ b/Classes/PicCastAppDelegate.m @@ -32,6 +32,16 @@ @synthesize navigationController; +//- (void)initialize { +// [[NSUserDefaults standardUserDefaults] +// registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys: +// NULL, @"facebookToken", +// NULL, @"facebookExpirationDate", +// NULL, @"twitterToken", +// [NSNumber numberWithBool:NO], @"haveSearched", +// nil]]; +//} + #pragma mark - #pragma mark Application lifecycle @@ -110,6 +120,41 @@ */ } +#pragma mark - +#pragma mark CustomSheeyit + +//+ (void) prompt:(NSString *)title withMessage:(NSString *)message andButtonTitle:(NSString *)buttonTitle withDelegate:(id)delegate { +// UIAlertView *prompt = [UIAlertView alloc]; +// prompt = [prompt initWithTitle:title message:message delegate:delegate cancelButtonTitle:buttonTitle otherButtonTitles:nil]; +// [prompt show]; +// [prompt release]; +//} +// +//+ (void) prompt:(NSString *)title withMessage:(NSString *)message andButtonTitle:(NSString *)buttonTitle { +// [self prompt:title withMessage:message andButtonTitle:buttonTitle withDelegate:self]; +//} +// +//- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url { +// return [facebook handleOpenURL:url]; +//} + ++ (NSString *) getDatabasePath { + NSString *databaseName = @"PicCastSqlTable.db"; + + NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *documentsDir = [documentPaths objectAtIndex:0]; + NSString *databasePath = [documentsDir stringByAppendingPathComponent:databaseName]; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + + if(![fileManager fileExistsAtPath:databasePath]) { + NSString *databasePathFromApp = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:databaseName]; + [fileManager copyItemAtPath:databasePathFromApp toPath:databasePath error:nil]; + } + + return databasePath; +} + #pragma mark - #pragma mark UITabBarControllerDelegate methods diff --git a/Classes/SectionDictionary.h b/Classes/SectionDictionary.h new file mode 100644 index 0000000..f227e44 --- /dev/null +++ b/Classes/SectionDictionary.h @@ -0,0 +1,23 @@ +// +// SectionDictionary.h +// PicCast +// +// Created by Matthew Handler on 4/23/11. +// Copyright 2011 Earl Industries. All rights reserved. +// + +#import + + +@interface SectionDictionary : NSObject { + NSInteger index; + NSMutableDictionary *indexMap; + NSMutableDictionary *theRealDictionary; +} + +- (void) appendObject:(id)anObject forKey:(id)aKey; +- (void) setObject:(id)anObject forKey:(id)aKey; +- (id) objectForIndex:(NSInteger)i; +- (NSString *) keyForIndex:(NSInteger)i; + +@end diff --git a/Classes/SectionDictionary.m b/Classes/SectionDictionary.m new file mode 100644 index 0000000..8845b2b --- /dev/null +++ b/Classes/SectionDictionary.m @@ -0,0 +1,64 @@ +// +// SectionDictionary.m +// PicCast +// +// Created by Matthew Handler on 4/23/11. +// Copyright 2011 Earl Industries. All rights reserved. +// + +#import "SectionDictionary.h" + + +@implementation SectionDictionary + +//@synthesize index; + +- (SectionDictionary *) init { + index = 0; + indexMap = [[[NSMutableDictionary alloc] init] retain]; + theRealDictionary = [[[NSMutableDictionary alloc] init] retain]; + [super init]; + + return self; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + return theRealDictionary; +} + +- (void) setObject:(id)anObject forKey:(id)aKey { + [indexMap setObject:aKey forKey:[NSString stringWithFormat:@"%d", index]]; + index++; + [theRealDictionary setObject:anObject forKey:aKey]; +} + +// lets you push objects into sections without worry about storage +- (void) appendObject:(id)anObject forKey:(id)aKey { + NSMutableArray *store = [theRealDictionary objectForKey:aKey]; + if (store == nil) { + NSMutableArray *store = [[[NSMutableArray alloc] init] autorelease]; + [store addObject:anObject]; + [theRealDictionary setObject:store forKey:aKey]; + } + else [store addObject:anObject]; + + NSLog(@"number of rows: %d", [[theRealDictionary objectForKey:aKey] count]); +} + +- (id) objectForIndex:(NSInteger)i { + return [theRealDictionary objectForKey:[indexMap objectForKey:[NSString stringWithFormat:@"%d", i]]]; +} + +- (NSString *) keyForIndex:(NSInteger)i { + return [indexMap objectForKey:[NSString stringWithFormat:@"%d", i]]; +} + +- (void) dealloc { + [theRealDictionary release]; + [indexMap release]; + [super dealloc]; +} + + +@end diff --git a/Classes/Topic.h b/Classes/Topic.h index 2ae6520..26f8c29 100644 --- a/Classes/Topic.h +++ b/Classes/Topic.h @@ -5,14 +5,13 @@ // Created by Matthew Handler on 4/16/11. // Copyright 2011 Earl Industries. All rights reserved. // - +#import "FMResultSet.h" #import @interface Topic : NSObject { NSString *title; - NSString *creator; NSString *description; NSDate *releaseDate; NSString *category; @@ -25,10 +24,13 @@ @property (nonatomic, copy) NSNumber *picCount; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *guid; -@property (nonatomic, copy) NSString *creator; @property (nonatomic, copy) NSString *description; @property (nonatomic, copy) NSDate *releaseDate; @property (nonatomic, copy) NSString *category; @property (nonatomic, copy) NSString *iconUrl; ++ (Topic *) initFromDatabaseRow:(FMResultSet *)result; ++ (NSString *) dateToString:(NSDate *)date; ++ (NSDate *) stringToDate:(NSString *)string; + @end diff --git a/Classes/Topic.m b/Classes/Topic.m index 712519b..65ea506 100644 --- a/Classes/Topic.m +++ b/Classes/Topic.m @@ -7,17 +7,69 @@ // #import "Topic.h" - +#import "FMDatabase.h" +#import "FMResultSet.h" @implementation Topic -@synthesize title, guid, creator, iconUrl, description, releaseDate, category, picCount; +@synthesize guid, title, iconUrl, description, releaseDate, category, picCount; + ++ (Topic *) initFromDatabaseRow:(FMResultSet *)result { + NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [dateFormatter setDateStyle:NSDateFormatterLongStyle]; + [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + [dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"US"] autorelease]]; + + Topic *topic = [[[Topic alloc] init] autorelease]; + + topic.guid = [result stringForColumn:@"link"]; + topic.title = [result stringForColumn:@"title"]; + topic.iconUrl = [result stringForColumn:@"iconUrl"]; + topic.description = [result stringForColumn:@"description"]; + topic.releaseDate = [Topic stringToDate:[result stringForColumn:@"releaseDate"]]; + topic.category = [result stringForColumn:@"category"]; + topic.picCount = [result intForColumn:@"picCount"]; + + return topic; +} + ++ (NSString *) dateToString:(NSDate *)date { + NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [dateFormatter setDateStyle:NSDateFormatterLongStyle]; + [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + [dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"US"] autorelease]]; + + return [dateFormatter stringFromDate:date]; +} + ++ (NSDate *) stringToDate:(NSString *)string { + NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; + [dateFormatter setDateStyle:NSDateFormatterLongStyle]; + [dateFormatter setTimeStyle:NSDateFormatterNoStyle]; + [dateFormatter setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"US"] autorelease]]; + + return [dateFormatter dateFromString:string]; +} + +- (void)serializeToDatabase:(FMDatabase *)db { + //NSLog(@"title: %@", title); + [db executeUpdate:@"INSERT INTO picCasts \ + (foreignId, link, title, description, releaseDate, category, iconUrl, picCount) \ + VALUES \ + (?, ?, ?, ?, ?, ?, ?, ?)", + 12, guid, title, description, [Topic dateToString:releaseDate], category, picCount]; + //NSLog(@"insert id:%d error: %@", [db lastInsertRowId], [db lastErrorMessage]); + + // [NSString stringWithFormat:@"number %d", i], + // [NSNumber numberWithInt:i], + // [NSDate date], + // [NSNumber numberWithFloat:2.2f]]; +} - (void)dealloc { [picCount release]; [guid release]; [title release]; - [creator release]; [description release]; [releaseDate release]; [category release]; diff --git a/Classes/TopicsViewController.h b/Classes/TopicsViewController.h index fac4e7d..c6de22f 100644 --- a/Classes/TopicsViewController.h +++ b/Classes/TopicsViewController.h @@ -12,14 +12,17 @@ //#import "PicDumpViewController.h" #import "SourcesEditViewController.h" #import "PhotoViewController.h" +#import "FMDatabase.h" +#import "SectionDictionary.h" @interface TopicsViewController : UIViewController { UINavigationController *topicsNavigationController; - NSMutableArray *topics; -// PicDumpViewController *picDumpViewController; + NSMutableArray *topics; // topics loaded from the web + SectionDictionary *tableDictionary; // structure to sync with table PhotoViewController *photoViewController; XMLParser *parser; HJObjManager* objMan; + FMDatabase* db; } - (IBAction)showSources:(id)sender; diff --git a/Classes/TopicsViewController.m b/Classes/TopicsViewController.m index 8bd4531..2548bf3 100644 --- a/Classes/TopicsViewController.m +++ b/Classes/TopicsViewController.m @@ -16,6 +16,9 @@ #import "HJObjManager.h" #import "HJManagedImageV.h" #import "SourcesEditViewController.h" +#import "SectionDictionary.h" +#import "FMDatabase.h" +#import "FMResultSet.h" @implementation TopicsViewController @@ -46,9 +49,87 @@ fileCache.fileAgeLimit = 60*60*24*7; //1 week [fileCache trimCacheUsingBackgroundThread]; + tableDictionary = [[[SectionDictionary alloc] init] retain]; + + NSString *path = [[NSBundle mainBundle] pathForResource:@"SampleDB" ofType:@"sqlite"]; + FMDatabase *db = [[FMDatabase alloc] initWithPath:path]; + [db open]; + + [self getTopicsFromDb]; + [self startParsing]; } +- (void) getTopicsFromDb { + // grab topics + FMResultSet *fResult= [db executeQuery:@"SELECT * FROM picCasts"]; //order by date, limite _max + + // put into sections + while([fResult next]) + { + [self addTopicToSectionDictionary:[Topic initFromDatabaseRow:fResult]]; + + } + [fResult close]; + + // put into data structure +} + +- (void) addTopicToSectionDictionary:(Topic *)topic { + NSLog(@"days apart: %d", [self daysApart:topic.releaseDate]); + switch ((int)[self daysApart:topic.releaseDate]) { + case 0: + [tableDictionary appendObject:topic forKey:@"Today"]; + break; + case 1: + [tableDictionary appendObject:topic forKey:@"Yesterday"]; + break; + default: + [tableDictionary appendObject:topic forKey:[Topic dateToString:topic.releaseDate]]; + break; + } +} + +//- (void) addTopic:(FMResultSet *)result forKey:(NSString *)key { +// NSMutableArray *store = [tableDictionary objectForKey:key]; +// if (store == nil) { +// NSMutableArray *store = [[[NSMutableArray alloc] init] autorelease]; +// [tableDictionary setObject:store forKey:key]; +// } +// [store addObject:result]; +//} + +- (NSTimeInterval) daysApart:(NSDate *)date { + //NSLog(@"datestring: %@", [Topic dateToString:date]); + NSDate *today = [NSDate date]; + +// NSDateFormatter *dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; +// [dateFormatter setDateFormat:@"yyyy-MM-dd"]; +// NSDate *published = [dateFormatter dateFromString:string]; + + return [today timeIntervalSinceDate:date]; +} + +// called after parsing is finished +- (void) updateTopicsFromWeb:(NSArray *)parsedTopics { + // add topics to sql + [db beginTransaction]; + for (Topic *topic in parsedTopics) { + // if topic is already in database + FMResultSet *rs = [db executeQuery:@"SELECT id FROM picCasts WHERE foreignId = ?", 10]; + if (![rs hasAnotherRow]) { + [topic serializeToDatabase:db]; + // add topics to structure + [self addTopicToSectionDictionary:topic]; + } + } + [db commit]; +// NSLog(@"%@", [db lastErrorMessage]); +// NSLog(@"%d", [db lastInsertRowId]); + // tell table to animate things being added + [self.tableView reloadData]; +} + - (void) sourcesEditViewControllerDidFinish:(SourcesEditViewController *)controller { [self dismissModalViewControllerAnimated:YES]; @@ -118,7 +199,8 @@ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. - return 2; + NSLog(@"number of sections: %d", [tableDictionary count]); + return [tableDictionary count]; } #pragma mark - @@ -138,14 +220,13 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Return the number of rows in the section. - return [topics count]; + NSLog(@"number of rows in section %d: %d", section, [[tableDictionary objectForIndex:section] count]); + return [[tableDictionary objectForIndex:section] count]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { - if (section == 0) - return @"Today"; - else - return @"Yesterday"; + NSLog(@"row title: %@", [tableDictionary keyForIndex:section]); + return [tableDictionary keyForIndex:section]; //how do you convert a key to index from mutabledictionary } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { @@ -258,7 +339,7 @@ } - (void)parser:(XMLParser *)parser didParseTopics:(NSArray *)parsedTopics { - [topics addObjectsFromArray:parsedTopics]; + [self updateTopicsFromWeb:parsedTopics]; // Three scroll view properties are checked to keep the user interface smooth during parse. When new objects are delivered // by the parser, the table view is reloaded to display them. If the table is reloaded while @@ -291,6 +372,7 @@ } - (void)dealloc { + [tableDictionary release]; [objMan release]; [topics release]; //[detailController release]; diff --git a/Classes/fmdb_Prefix.pch b/Classes/fmdb_Prefix.pch new file mode 100644 index 0000000..b8c83c3 --- /dev/null +++ b/Classes/fmdb_Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'fmdb' target in the 'fmdb' project. +// + +#ifdef __OBJC__ + #import +#endif -- cgit v1.2.3