/samples/Objective-C++/objsql.mm
Objective C++ | 1372 lines | 909 code | 245 blank | 218 comment | 207 complexity | 06e91f14fdd4bec6d22ef5517687de0d MD5 | raw file
- /*
- * objsql.m - implementaion simple persistence layer using objcpp.h
- * ========
- *
- * Created by John Holdsworth on 01/04/2009.
- * Copyright 2009 John Holdsworth.
- *
- * $Id: //depot/4.4/ObjCpp/objsql.mm#11 $
- * $DateTime: 2012/09/05 00:20:47 $
- *
- * C++ classes to wrap up XCode classes for operator overload of
- * useful operations such as access to NSArrays and NSDictionary
- * by subscript or NSString operators such as + for concatenation.
- *
- * This works as the Apple Objective-C compiler supports source
- * which mixes C++ with objective C. To enable this: for each
- * source file which will include/import this header file, select
- * it in Xcode and open it's "Info". To enable mixed compilation,
- * for the file's "File Type" select: "sourcecode.cpp.objcpp".
- *
- * For bugs or ommisions please email objcpp@johnholdsworth.com
- *
- * Home page for updates and docs: http://objcpp.johnholdsworth.com
- *
- * You may make commercial use of this source in applications without
- * charge but not sell it as source nor can you remove this notice from
- * this source if you redistribute. You can make any changes you like
- * to this code before redistribution but please annotate them below.
- *
- * If you find it useful please send a donation via paypal to account
- * objcpp@johnholdsworth.com. Thanks.
- *
- * THIS CODE IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND EITHER
- * EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
- *
- * IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
- * WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
- * THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
- * ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT
- * OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
- * TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED
- * BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH
- * ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN
- * ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
- *
- */
- #import <objc/runtime.h>
- #import <sqlite3.h>
- #import "objsql.h"
- #if 0
- #ifdef OODEBUG
- #define OODEBUG_SQL 1
- #endif
- #endif
- OOOODatabase OODB;
- static NSString *kOOObject = @"__OOOBJECT__", *kOOInsert = @"__ISINSERT__", *kOOUpdate = @"__ISUPDATE__", *kOOExecSQL = @"__OOEXEC__";
- #pragma mark OORecord abstract superclass for records
- @implementation OORecord
- + (id)record OO_AUTORETURNS {
- return OO_AUTORELEASE( [[self alloc] init] );
- }
- + (id)insert OO_AUTORETURNS {
- OORecord *record = [self record];
- [record insert];
- return record;
- }
- + (id)insertWithParent:(id)parent {
- return [[OODatabase sharedInstance] copyJoinKeysFrom:parent to:[self insert]];
- }
- - (id)insert { [[OODatabase sharedInstance] insert:self]; return self; }
- - (id)delete { [[OODatabase sharedInstance] delete:self]; return self; }
- - (void)update { [[OODatabase sharedInstance] update:self]; }
- - (void)indate { [[OODatabase sharedInstance] indate:self]; }
- - (void)upsert { [[OODatabase sharedInstance] upsert:self]; }
- - (int)commit { return [[OODatabase sharedInstance] commit]; }
- - (int)rollback { return [[OODatabase sharedInstance] rollback]; }
- // to handle null values for ints, floats etc.
- - (void)setNilValueForKey:(NSString *)key {
- static OOReference<NSValue *> zeroForNull;
- if ( !zeroForNull )
- zeroForNull = [NSNumber numberWithInt:0];
- [self setValue:zeroForNull forKey:key];
- }
- + (OOArray<id>)select {
- return [[OODatabase sharedInstance] select:nil intoClass:self joinFrom:nil];
- }
- + (OOArray<id>)select:(cOOString)sql {
- return [[OODatabase sharedInstance] select:sql intoClass:self joinFrom:nil];
- }
- + (OOArray<id>)selectRecordsRelatedTo:(id)parent {
- return [[OODatabase sharedInstance] select:nil intoClass:self joinFrom:parent];
- }
- - (OOArray<id>)select {
- return [[OODatabase sharedInstance] select:nil intoClass:[self class] joinFrom:self];
- }
- /**
- import a flat file with column values separated by the delimiter specified into
- the table associated with this class.
- */
- + (int)importFrom:(OOFile &)file delimiter:(cOOString)delim {
- OOArray<id> rows = [OOMetaData import:file.string() intoClass:self delimiter:delim];
- [[OODatabase sharedInstance] insertArray:rows];
- return [OODatabase commit];
- }
- /**
- Export a flat file with all rows in the table associated with the record subclass.
- */
- + (BOOL)exportTo:(OOFile &)file delimiter:(cOOString)delim {
- return file.save( [OOMetaData export:[self select] delimiter:delim] );
- }
- /**
- populate a view with the string values taken from the ivars of the record
- */
- - (void)bindToView:(OOView *)view delegate:(id)delegate {
- [OOMetaData bindRecord:self toView:view delegate:delegate];
- }
- /**
- When the delegate method os sent use this method to update the record's values
- */
- - (void)updateFromView:(OOView *)view {
- [OOMetaData updateRecord:self fromView:view];
- }
- /**
- Default description is dictionary containing values of all ivars
- */
- - (NSString *)description {
- OOMetaData *metaData = [OOMetaData metaDataForClass:[self class]];
- // hack required where record contains a field "description" to avoid recursion
- OOStringArray ivars; ivars <<= *metaData->ivars; ivars -= "description";
- return [*[metaData encode:[self dictionaryWithValuesForKeys:ivars]] description];
- }
- @end
- #pragma mark OOAdaptor - all methods required by objsql to access a database
- /**
- An internal class representing the interface to a particular database, in this case sqlite3.
- */
- @interface OOAdaptor : NSObject {
- sqlite3 *db;
- sqlite3_stmt *stmt;
- struct _str_link {
- struct _str_link *next; char str[1];
- } *strs;
- OO_UNSAFE OODatabase *owner;
- }
- - initPath:(cOOString)path database:(OODatabase *)database;
- - (BOOL)prepare:(cOOString)sql;
- - (BOOL)bindCols:(cOOStringArray)columns values:(cOOValueDictionary)values startingAt:(int)pno bindNulls:(BOOL)bindNulls;
- - (OOArray<id>)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(OOMetaData *)metaData;
- - (sqlite_int64)lastInsertRowID;
- @end
- @interface NSData(OOExtras)
- - initWithDescription:(NSString *)description;
- @end
- #pragma mark OODatabase is the low level interface to a particular database
- @implementation OODatabase
- static OOReference<OODatabase *> sharedInstance;
- /**
- By default database file is "objsql.db" in the user/application's "Documents" directory
- and a single shared OODatabase instance used for all db operations.
- */
- + (OODatabase *)sharedInstance {
- if ( !sharedInstance )
- [self sharedInstanceForPath:OODocument("objsql.db").path()];
- return sharedInstance;
- }
- /**
- Shared instance can be switched between any file paths.
- */
-
- + (OODatabase *)sharedInstanceForPath:(cOOString)path {
- if ( !!sharedInstance )
- sharedInstance = OONil;
- if ( !!path )
- OO_RELEASE( sharedInstance = [[OODatabase alloc] initPath:path] );
- return sharedInstance;
- }
- + (BOOL)exec:(cOOString)fmt, ... {
- va_list argp; va_start(argp, fmt);
- NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp];
- va_end( argp );
- return [[self sharedInstance] exec:OO_AUTORELEASE( sql )];
- }
- + (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass joinFrom:(id)parent {
- return [[self sharedInstance] select:select intoClass:recordClass joinFrom:parent];
- }
- + (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass {
- return [[self sharedInstance] select:select intoClass:recordClass joinFrom:nil];
- }
- + (OOArray<id>)select:(cOOString)select {
- return [[self sharedInstance] select:select intoClass:nil joinFrom:nil];
- }
- + (int)insertArray:(const OOArray<id> &)objects { return [[self sharedInstance] insertArray:objects]; }
- + (int)deleteArray:(const OOArray<id> &)objects { return [[self sharedInstance] deleteArray:objects]; }
- + (int)insert:(id)object { return [[self sharedInstance] insert:object]; }
- + (int)delete:(id)object { return [[self sharedInstance] delete:object]; }
- + (int)update:(id)object { return [[self sharedInstance] update:object]; }
- + (int)indate:(id)object { return [[self sharedInstance] indate:object]; }
- + (int)upsert:(id)object { return [[self sharedInstance] upsert:object]; }
- + (int)commit { return [[self sharedInstance] commit]; }
- + (int)rollback { return [[self sharedInstance] rollback]; }
- + (int)commitTransaction { return [[OODatabase sharedInstance] commitTransaction]; }
- /**
- Designated initialiser for OODatabase instances. Generally only the shared instance is used
- and the OODatabase class object is messaged instead.
- */
- - initPath:(cOOString)path {
- if ( self = [super init] )
- OO_RELEASE( adaptor = [[OOAdaptor alloc] initPath:path database:self] );
- return self;
- }
- /**
- Automatically register all classes which are subclasses of a record abstract superclass (e.g. OORecord).
- */
- - (OOStringArray)registerSubclassesOf:(Class)recordSuperClass {
- int numClasses = objc_getClassList( NULL, 0 );
- Class *classes = (Class *)malloc( sizeof *classes * numClasses );
- OOArray<Class> viewClasses;
- OOStringArray classNames;
- // scan all registered classes for relevant subclasses
- numClasses = objc_getClassList( classes, numClasses );
- for ( int c=0 ; c<numClasses ; c++ ) {
- Class superClass = classes[c];
- if ( class_getName( superClass )[0] != '_' )
- while ( (superClass = class_getSuperclass( superClass )) )
- if ( superClass == recordSuperClass ) {
- if ( [classes[c] respondsToSelector:@selector(ooTableSql)] )
- viewClasses += classes[c];
- else {
- [[OODatabase sharedInstance] tableMetaDataForClass:classes[c]];
- classNames += class_getName( classes[c] );
- }
- break;
- }
- }
- // delay creation views until after tables
- for ( int c=0 ; c<viewClasses ; c++ ) {
- [[OODatabase sharedInstance] tableMetaDataForClass:viewClasses[c]];
- classNames += class_getName( viewClasses[c] );
- }
- // return classes in order registered
- free( classes );
- return classNames;
- }
- /**
- Register a list of classes before using them so OODatabase can determine the relationships between them.
- */
- - (void)registerTableClassesNamed:(cOOStringArray)classes {
- for ( NSString *tableClass in *classes )
- [self tableMetaDataForClass:[[NSBundle mainBundle] classNamed:tableClass]];
- }
- /**
- Send any SQL to the database. Sql is a format string so escape any '%' characters using '%%'.
- Any results returned are placed as an array of dictionary values in the database->results.
- */
-
- - (BOOL)exec:(cOOString)fmt, ... {
- va_list argp; va_start(argp, fmt);
- NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp];
- va_end( argp );
- results = [self select:sql intoClass:NULL joinFrom:nil];
- OO_RELEASE( sql );
- return !errcode;
- }
- /**
- Return a single value from row 1, column one from sql sent to the database as a string.
- */
- - (OOString)stringForSql:(cOOString)fmt, ... {
- va_list argp; va_start(argp, fmt);
- NSString *sql = OO_AUTORELEASE( [[NSString alloc] initWithFormat:fmt arguments:argp] );
- va_end( argp );
- if( [self exec:"%@", sql] && results > 0 ) {
- NSString *aColumnName = [[**results[0] allKeys] objectAtIndex:0];
- return [(NSNumber *)(*results[0])[aColumnName] stringValue];
- }
- else
- return nil;
- }
- /**
- Used to initialise new child records automatically from parent in relation.
- */
- - (id)copyJoinKeysFrom:(id)parent to:(id)newChild {
- OOMetaData *parentMetaData = [self tableMetaDataForClass:[parent class]],
- *childMetaData = [self tableMetaDataForClass:[newChild class]];
- OOStringArray commonColumns = [parentMetaData naturalJoinTo:childMetaData->columns]; ////
- OOValueDictionary keyValues = [parentMetaData encode:[parent dictionaryWithValuesForKeys:commonColumns]];
- [newChild setValuesForKeysWithDictionary:[childMetaData decode:keyValues]];
- return newChild;
- }
- /**
- Build a where clause for the columns specified.
- */
- - (OOString)whereClauseFor:(cOOStringArray)columns values:(cOOValueDictionary)values qualifyNulls:(BOOL)qualifyNulls {
- OOString out;
- for ( int i=0 ; i<columns ; i++ ) {
- NSString *name = *columns[i];
- const char *prefix = i==0 ?"\nwhere":" and";
- id value = values[name];
- if ( value == OONull )
- out += qualifyNulls ? OOFormat( @"%s\n\t%@ is NULL", prefix, name ) : @"";
- else if ( !qualifyNulls && [value isKindOfClass:[NSString class]] &&
- [value rangeOfString:@"%"].location != NSNotFound )
- out += OOFormat( @"%s\n\t%@ LIKE ?", prefix, name );
- else
- out += OOFormat( @"%s\n\t%@ = ?", prefix, name );
- }
- return out;
- }
- /**
- Prepare the sql passed in adding a where clause with bindings for a join to values taken from the parent record.
- */
- - (BOOL)prepareSql:(OOString &)sql joinFrom:(id)parent toTable:(OOMetaData *)metaData {
- OOValueDictionary joinValues;
- OOStringArray sharedColumns;
- if ( parent ) {
- OOMetaData *parentMetaData = [self tableMetaDataForClass:[parent class]];
- sharedColumns = [parentMetaData naturalJoinTo:metaData->joinableColumns];
- joinValues = [parentMetaData encode:[parent dictionaryWithValuesForKeys:sharedColumns]];
- sql += [self whereClauseFor:sharedColumns values:joinValues qualifyNulls:NO];
- }
- if ( [metaData->recordClass respondsToSelector:@selector(ooOrderBy)] )
- sql += OOFormat( @"\norder by %@", [metaData->recordClass ooOrderBy] );
- #ifdef OODEBUG_SQL
- NSLog( @"-[OOMetaData prepareSql:] %@\n%@", *sql, *joinValues );
- #endif
- if ( ![*adaptor prepare:sql] )
- return NO;
- return !parent || [*adaptor bindCols:sharedColumns values:joinValues startingAt:1 bindNulls:NO];
- }
- /**
- Determine a list of the tables which have a natural join to the record passed in.
- If the record is a specific instance of from a table this should determine if
- there are any record which exist using the join.
- */
- - (OOArray<OOMetaData *>)tablesRelatedByNaturalJoinFrom:(id)record {
- OOMetaData *metaData = [record class] == [OOMetaData class] ?
- record : [self tableMetaDataForClass:[record class]];
- OOStringArray tablesWithNaturalJoin;
- tablesWithNaturalJoin <<= metaData->tablesWithNaturalJoin;
- if ( record && record != metaData )
- for ( int i=0 ; i<tablesWithNaturalJoin ; i++ ) {
- OOString sql = OOFormat( @"select count(*) as result from %@", **tablesWithNaturalJoin[i] );
- OOMetaData *childMetaData = tableMetaDataByClassName[tablesWithNaturalJoin[i]];
- [self prepareSql:sql joinFrom:record toTable:childMetaData];
- OOArray<OODictionary<NSNumber *> > tmpResults = [*adaptor bindResultsIntoInstancesOfClass:NULL metaData:nil];
- if ( ![(*tmpResults[0])["result"] intValue] )
- ~tablesWithNaturalJoin[i--];
- }
- return tableMetaDataByClassName[+tablesWithNaturalJoin];
- }
- /**
- Perform a select from a table on the database using either the sql specified
- orselect all columns from the table associated with the record class passed in.
- If a parent is passed in make a natural join from that record.
- */
- - (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass joinFrom:(id)parent {
- OOMetaData *metaData = [self tableMetaDataForClass:recordClass ? recordClass : [parent class]];
- OOString sql = !select ?
- OOFormat( @"select %@\nfrom %@", *(metaData->outcols/", "), *metaData->tableName ) : *select;
- if ( ![self prepareSql:sql joinFrom:parent toTable:metaData] )
- return nil;
- return [*adaptor bindResultsIntoInstancesOfClass:recordClass metaData:metaData];
- }
- - (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass {
- return [self select:select intoClass:recordClass joinFrom:nil];
- }
- - (OOArray<id>)select:(cOOString)select {
- return [self select:select intoClass:nil joinFrom:nil];
- }
- /**
- Returns sqlite3 row identifier for a record instance.
- */
- - (long long)rowIDForRecord:(id)record {
- OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
- OOString sql = OOFormat( @"select ROWID from %@", *metaData->tableName );
- OOArray<OODictionary<NSNumber *> > idResults = [self select:sql intoClass:nil joinFrom:record];
- return [*(*idResults[0])[@"rowid"] longLongValue];
- }
- /**
- Returns sqlite3 row identifier for last inserted record.
- */
- - (long long)lastInsertRowID {
- return [*adaptor lastInsertRowID];
- }
- /**
- Insert an array of record objects into the database. This needs to be commited to take effect.
- */
- - (int)insertArray:(const OOArray<id> &)objects {
- int count = 0;
- for ( id object in *objects )
- count = [self insert:object];
- return count;
- }
- /**
- Delete an array of record objects from the database. This needs to be commited to take effect.
- */
- - (int)deleteArray:(const OOArray<id> &)objects {
- int count = 0;
- for ( id object in *objects )
- if ( ![object respondsToSelector:@selector(delete)] )
- count = [self delete:object];
- else {
- [object delete];
- count++;
- }
- return count;
- }
- /**
- Insert the values of the record class instance at the time this method was called into the db.
- (must be commited to take effect). Returns the total number of outstanding inserts/updates/deletes.
- */
- - (int)insert:(id)record {
- return transaction += OOValueDictionary( kOOObject, record, kOOInsert, kCFNull, nil );
- }
- /**
- Use the values of the record instance at the time this method is called in a where clause to
- delete from the database when commit is called. Returns the total number of outstanding
- inserts/updates/deletes.
- */
- - (int)delete:(id)record {
- return transaction += OOValueDictionary( kOOObject, record, nil );
- }
- /**
- Call this method if you intend to make changes to the record object and save them to the database.
- This takes a snapshot of the previous values to use as a key for the update operation when "commit"
- is called. Returns the total number of outstanding inserts/updates/deletes.
- */
- - (int)update:(id)record {
- OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
- OOValueDictionary oldValues = [metaData encode:[record dictionaryWithValuesForKeys:metaData->columns]];
- for ( NSString *key in *metaData->tocopy )
- OO_RELEASE( oldValues[key] = [oldValues[key] copy] );
- oldValues[kOOUpdate] = OONull;
- oldValues[kOOObject] = record;
- return transaction += oldValues;
- }
- /**
- Inserts a record into the database the deletes any previous record with the same key.
- This ensures the record's rowid changes if this is used by child records.
- */
- - (int)indate:(id)record {
- OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
- OOString sql = OOFormat( @"select rowid from %@", *metaData->tableName );
- OOArray<id> existing = [self select:sql intoClass:nil joinFrom:record];
- int count = [self insert:record];
- for ( NSDictionary *exist in *existing ) {
- OOString sql = OOFormat( @"delete from %@ where rowid = %ld", *metaData->tableName,
- (long)[[exist objectForKey:@"rowid"] longLongValue] );
- transaction += OOValueDictionary( kOOExecSQL, *sql, nil );
- }
- return count;
- }
- /**
- Inserts a record into the database unless another record with the same key column values
- exists in which case it will do an update of the previous record (preserving the ROWID.)
- */
- - (int)upsert:(id)record {
- OOArray<id> existing = [self select:nil intoClass:[record class] joinFrom:record];
- if ( existing > 1 )
- OOWarn( @"-[ODatabase upsert:] Duplicate record for upsert: %@", record );
- if ( existing > 0 ) {
- [self update:existing[0]];
- (*transaction[-1])[kOOObject] = record;
- return transaction;
- }
- else
- return [self insert:record];
- }
- /**
- Commit all pending inserts, updates and deletes to the database. Use commitTransaction to perform
- this inside a database transaction.
- */
- - (int)commit {
- int commited = 0;
- for ( int i=0 ; i<transaction ; i++ ) {
- OOValueDictionary values = transaction[i];
- OOString exec = (NSMutableString *)~values[kOOExecSQL];
- if ( !!exec ) {
- if ( ![self exec:@"%@", *exec] )
- OOWarn( @"-[ODatabase commit] Error in transaction exec: %@ - %s", *exec, errmsg );
- continue;
- }
- OORef<NSObject *> object = *values[kOOObject]; values -= kOOObject;
- BOOL isInsert = !!~values[kOOInsert], isUpdate = !!~values[kOOUpdate];
- OOMetaData *metaData = [self tableMetaDataForClass:[object class]];
- OOValueDictionary newValues = [metaData encode:[object dictionaryWithValuesForKeys:metaData->columns]];
- OOStringArray changedCols;
- if ( isUpdate ) {
- for ( NSString *name in *metaData->columns )
- if ( ![*newValues[name] isEqual:values[name]] )
- changedCols += name;
- }
- else
- values = newValues;
- OOString sql = isInsert ?
- OOFormat( @"insert into %@ (%@) values (", *metaData->tableName, *(metaData->columns/", ") ) :
- OOFormat( isUpdate ? @"update %@ set" : @"delete from %@", *metaData->tableName );
- int nchanged = changedCols;
- if ( isUpdate && nchanged == 0 ) {
- OOWarn( @"%s %@ (%@)", errmsg = (char *)"-[ODatabase commit:] Update of unchanged record", *object, *(lastSQL = sql) );
- continue;
- }
- for ( int i=0 ; i<nchanged ; i++ )
- sql += OOFormat( @"%s\n\t%@ = ?", i==0 ? "" : ",", **changedCols[i] );
- if ( isInsert ) {
- OOString quote = "?", commaQuote = ", ?";
- for ( int i=0 ; i<metaData->columns ; i++ )
- sql += i==0 ? quote : commaQuote;
- sql += ")";
- }
- else
- sql += [self whereClauseFor:metaData->columns values:values qualifyNulls:YES];
- #ifdef OODEBUG_SQL
- NSLog( @"-[OODatabase commit]: %@ %@", *sql, *values );
- #endif
- if ( ![*adaptor prepare:sql] )
- continue;
- if ( isUpdate )
- [*adaptor bindCols:changedCols values:newValues startingAt:1 bindNulls:YES];
- [*adaptor bindCols:metaData->columns values:values startingAt:1+nchanged bindNulls:isInsert];
- [*adaptor bindResultsIntoInstancesOfClass:nil metaData:metaData];
- commited += updateCount;
- }
- transaction = nil;
- return commited;
- }
- /**
- Commit all pending inserts, updates, deletes to the database inside a transaction.
- */
- - (int)commitTransaction {
- [self exec:"BEGIN TRANSACTION"];
- int updated = [self commit];
- return [self exec:"COMMIT"] ? updated : 0;
- }
- /**
- Rollback any outstanding inserts, updates, or deletes. Please note updated values
- are also rolled back inside the actual record in the application as well.
- */
- - (int)rollback {
- for ( NSMutableDictionary *d in *transaction ) {
- OODictionary<id> values = d;
- if ( !!~values[kOOUpdate] ) {
- OORef<OORecord *> record = ~values[kOOObject];
- OOMetaData *metaData = [self tableMetaDataForClass:[*record class]];
- #ifndef OO_ARC
- for ( NSString *name in *metaData->boxed )
- OO_RELEASE( (id)[[*record valueForKey:name] pointerValue] );
- #endif
- [*record setValuesForKeysWithDictionary:[metaData decode:values]];
- }
- }
- return (int)[*~transaction count];
- }
- /**
- Find/create an instance of the OOMetaData class which describes a record class and its associated table.
- If the table does not exist it will be created along with indexes for columns/ivars which have
- upper case names for use in joins. Details of the tables parameters can be controlled by using
- methods in the OOTableCustomisation protool. If it does not exist a meta table class which
- prepresents OOMetaData records themselves is also created from which the list of all registered
- tables can be selected.
- */
-
- - (OOMetaData *)tableMetaDataForClass:(Class)recordClass {
- if ( !recordClass || recordClass == [OOMetaData class] )
- return [OOMetaData metaDataForClass:[OOMetaData class]];
- OOString className = class_getName( recordClass );
- OOMetaData *metaData = tableMetaDataByClassName[className];
- if ( !metaData ) {
- metaData = [OOMetaData metaDataForClass:recordClass];
- #ifdef OODEBUG_SQL
- NSLog(@"\n%@", *metaData->createTableSQL);
- #endif
- if ( metaData->tableName[0] != '_' &&
- [self stringForSql:"select count(*) from sqlite_master where name = '%@'",
- *metaData->tableName] == "0" )
- if ( [self exec:"%@", *metaData->createTableSQL] )
- for ( NSString *idx in *metaData->indexes )
- if ( ![self exec:idx] )
- OOWarn( @"-[OOMetaData tableMetaDataForClass:] Error creating index: %@", idx );
- tableMetaDataByClassName[className] = metaData;
- }
- return metaData;
- }
- @end
- #pragma mark OOAdaptor - implements all access to a particular database
- @implementation OOAdaptor
- /**
- Connect to/create sqlite3 database
- */
- - (OOAdaptor *)initPath:(cOOString)path database:(OODatabase *)database {
- if ( self = [super init] ) {
- owner = database;
- OOFile( OOFile( path ).directory() ).mkdir();
- if ( (owner->errcode = sqlite3_open( path, &db )) != SQLITE_OK ) {
- OOWarn( @"-[OOAdaptor initPath:database:] Error opening database at path: %@", *path );
- return nil;
- }
- }
- return self;
- }
- /**
- Prepare a sql statement after which values can be bound and results returned.
- */
- - (BOOL)prepare:(cOOString)sql {
- if ( (owner->errcode = sqlite3_prepare_v2( db, owner->lastSQL = sql, -1, &stmt, 0 )) != SQLITE_OK )
- OOWarn(@"-[OOAdaptor prepare:] Could not prepare sql: \"%@\" - %s", *owner->lastSQL, owner->errmsg = (char *)sqlite3_errmsg( db ) );
- return owner->errcode == SQLITE_OK;
- }
- - (int)bindValue:(id)value asParameter:(int)pno {
- #ifdef OODEBUG_BIND
- NSLog( @"-[OOAdaptor bindValue:bindValue:] bind parameter #%d as: %@", pno, value );
- #endif
- if ( !value || value == OONull )
- return sqlite3_bind_null( stmt, pno );
- #if OOSQL_THREAD_SAFE_BUT_USES_MORE_MEMORY
- else if ( [value isKindOfClass:[NSString class]] )
- return sqlite3_bind_text( stmt, pno, [value UTF8String], -1, SQLITE_STATIC );
- #else
- else if ( [value isKindOfClass:[NSString class]] ) {
- int len = (int)[value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
- struct _str_link *str = (struct _str_link *)malloc( sizeof *str + len );
- str->next = strs;
- strs = str;
- [value getCString:str->str maxLength:len+1 encoding:NSUTF8StringEncoding];
- return sqlite3_bind_text( stmt, pno, str->str, len, SQLITE_STATIC );
- }
- #endif
- else if ( [value isKindOfClass:[NSData class]] )
- return sqlite3_bind_blob( stmt, pno, [value bytes], (int)[value length], SQLITE_STATIC );
- const char *type = [value objCType];
- if ( type )
- switch ( type[0] ) {
- case 'c': case 's': case 'i': case 'l':
- case 'C': case 'S': case 'I': case 'L':
- return sqlite3_bind_int( stmt, pno, [value intValue] );
- case 'q': case 'Q':
- return sqlite3_bind_int64( stmt, pno, [value longLongValue] );
- case 'f': case 'd':
- return sqlite3_bind_double( stmt, pno, [value doubleValue] );
- }
- OOWarn( @"-[OOAdaptor bindValue:bindValue:] Undefined type in bind of parameter #%d: %s, value: %@", pno, type, value );
- return -1;
- }
- /**
- Bind parameters from a prepared SQL statement. The "objCType" method is used to determine the type to bind.
- */
- - (BOOL)bindCols:(cOOStringArray)columns values:(cOOValueDictionary)values startingAt:(int)pno bindNulls:(BOOL)bindNulls {
- int errcode;
- for ( NSString *name in *columns )
- if ( bindNulls || *values[name] != OONull )
- if ( (errcode = [self bindValue:values[name] asParameter:pno++]) != SQLITE_OK )
- OOWarn( @"-[OOAdaptor bindCols:...] Bind failed column: %@ - %s (%d)", name, owner->errmsg = (char *)sqlite3_errmsg( db ), owner->errcode = errcode );
- return owner->errcode == SQLITE_OK;
- }
- /**
- Return a dictionary containing the values for a row returned by the database from a select.
- These values need to be decoded using a classes metadata to set the ivar values later.
- */
- - (OOValueDictionary)valuesForNextRow {
- int ncols = sqlite3_column_count( stmt );
- OOValueDictionary values;
- for ( int i=0 ; i<ncols ; i++ ) {
- OOString name = sqlite3_column_name( stmt, i );
- id value = nil;
- switch ( sqlite3_column_type( stmt, i ) ) {
- case SQLITE_NULL:
- value = OONull;
- break;
- case SQLITE_INTEGER:
- value = [[NSNumber alloc] initWithLongLong:sqlite3_column_int64( stmt, i )];
- break;
- case SQLITE_FLOAT:
- value = [[NSNumber alloc] initWithDouble:sqlite3_column_double( stmt, i )];
- break;
- case SQLITE_TEXT: {
- const unsigned char *bytes = sqlite3_column_text( stmt, i );
- value = [[NSMutableString alloc] initWithBytes:bytes
- length:sqlite3_column_bytes( stmt, i)
- encoding:NSUTF8StringEncoding];
- }
- break;
- case SQLITE_BLOB: {
- const void *bytes = sqlite3_column_blob( stmt, i );
- value = [[NSData alloc] initWithBytes:bytes length:sqlite3_column_bytes( stmt, i )];
- }
- break;
- default:
- OOWarn( @"-[OOAdaptor valuesForNextRow:] Invalid type on bind of ivar %@: %d", *name, sqlite3_column_type( stmt, i ) );
- }
- values[name] = value;
- OO_RELEASE( value );
- }
- return values;
- }
- /**
- Create instances of the recordClass to store results from a database select or if the record
- class is not present return a list of dictionary objects with the raw results.
- */
- - (OOArray<id>)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(OOMetaData *)metaData {
- OOArray<id> out;
- BOOL awakeFromDB = [recordClass instancesRespondToSelector:@selector(awakeFromDB)];
- while( (owner->errcode = sqlite3_step( stmt )) == SQLITE_ROW ) {
- OOValueDictionary values = [self valuesForNextRow];
- if ( recordClass ) {
- id record = [[recordClass alloc] init];
- [record setValuesForKeysWithDictionary:[metaData decode:values]];
- if ( awakeFromDB )
- [record awakeFromDB];
- out += record;
- OO_RELEASE( record );
- }
- else
- out += values;
- }
- if ( owner->errcode != SQLITE_DONE )
- OOWarn(@"-[OOAdaptor bindResultsIntoInstancesOfClass:metaData:] Not done (bind) stmt: %@ - %s", *owner->lastSQL, owner->errmsg = (char *)sqlite3_errmsg( db ) );
- else {
- owner->errcode = SQLITE_OK;
- out.alloc();
- }
- while ( strs != NULL ) {
- struct _str_link *next = strs->next;
- free( strs );
- strs = next;
- }
- owner->updateCount = sqlite3_changes( db );
- sqlite3_finalize( stmt );
- return out;
- }
- - (sqlite_int64)lastInsertRowID {
- return sqlite3_last_insert_rowid( db );
- }
- - (void) dealloc {
- sqlite3_close( db );
- OO_DEALLOC( super );
- }
- @end
- #pragma mark OOMetaData instances represent a table in the database and it's record class
- @implementation OOMetaData
- static OODictionary<OOMetaData *> metaDataByClass;
- static OOMetaData *tableOfTables;
- + (NSString *)ooTableTitle { return @"Table MetaData"; }
- + (OOMetaData *)metaDataForClass:(Class)recordClass OO_RETURNS {
- if ( !tableOfTables )
- OO_RELEASE( tableOfTables = [[OOMetaData alloc] initClass:[OOMetaData class]] );
- OOMetaData *metaData = metaDataByClass[recordClass];
- if ( !metaData )
- OO_RELEASE( metaData = [[OOMetaData alloc] initClass:recordClass] );
- return metaData;
- }
- + (OOArray<id>)selectRecordsRelatedTo:(id)record {
- return [[OODatabase sharedInstance] tablesRelatedByNaturalJoinFrom:record];
- }
- - initClass:(Class)aClass {
- if ( !(self = [super init]) )
- return self;
- recordClass = aClass;
- metaDataByClass[recordClass] = self;
- recordClassName = class_getName( recordClass );
- tableTitle = [recordClass respondsToSelector:@selector(ooTableTitle)] ?
- [recordClass ooTableTitle] : *recordClassName;
- tableName = [recordClass respondsToSelector:@selector(ooTableName)] ?
- [recordClass ooTableName] : *recordClassName;
-
- if ( aClass == [OOMetaData class] ) {
- ivars = columns = outcols = boxed = unbox =
- "tableTitle tableName recordClassName keyColumns ivars columns outcols";
- return self;
- }
- createTableSQL = OOFormat( @"create table %@ (", *tableName );
- OOArray<Class> hierarchy;
- do
- hierarchy += aClass;
- while ( (aClass = [aClass superclass]) && aClass != [NSObject class] );
- for ( int h=(int)hierarchy-1 ; h>=0 ; h-- ) {
- aClass = (Class)hierarchy[h]; ///
- Ivar *ivarInfo = class_copyIvarList( aClass, NULL );
- if ( !ivarInfo )
- continue;
- for ( int in=0 ; ivarInfo[in] ; in++ ) {
- OOString columnName = ivar_getName( ivarInfo[in] );
- ivars += columnName;
- OOString type = types[columnName] = ivar_getTypeEncoding( ivarInfo[in] ), dbtype = "";
- SEL columnSel = sel_getUid(columnName);
- switch ( type[0] ) {
- case 'c': case 's': case 'i': case 'l':
- case 'C': case 'S': case 'I': case 'L':
- case 'q': case 'Q':
- dbtype = @"int";
- break;
- case 'f': case 'd':
- dbtype = @"real";
- break;
- case '{':
- static OOPattern isOORef( "=\"ref\"@\"NS" );
- if( !(type & isOORef) )
- OOWarn( @"-[OOMetaData initClass:] Invalid structure type for ivar %@ in class %@: %@", *columnName, *recordClassName, *type );
- boxed += columnName;
- if ( ![recordClass instancesRespondToSelector:columnSel] ) {
- unbox += columnName;
- if ( [[recordClass superclass] instancesRespondToSelector:columnSel] )
- OOWarn( @"-[OOMetaData initClass:] Superclass of class %@ is providing method for column: %@", *recordClassName, *columnName );
- }
- case '@':
- static OOPattern isNSString( "NS(Mutable)?String\"" ),
- isNSDate( "\"NSDate\"" ), isNSData( "NS(Mutable)?Data\"" );
- if ( type & isNSString )
- dbtype = @"text";
- else if ( type & isNSDate ) {
- dbtype = @"real";
- dates += columnName;
- }
- else {
- if ( !(type & isNSData) )
- archived += columnName;
- blobs += columnName;
- dbtype = @"blob";
- }
- break;
- default:
- OOWarn( @"-[OOMetaData initClass:] Unknown data type '%@' in class %@", *type, *tableName );
- archived += columnName;
- blobs += columnName;
- dbtype = @"blob";
- break;
- }
- if ( dbtype == @"text" )
- tocopy += columnName;
- if ( columnName == @"rowid" || columnName == @"ROWID" ||
- columnName == @"OID" || columnName == @"_ROWID_" ) {
- outcols += columnName;
- continue;
- }
- createTableSQL += OOFormat(@"%s\n\t%@ %@ /* %@ */",
- !columns?"":",", *columnName, *dbtype, *type );
- if ( iswupper( columnName[columnName[0] != '_' ? 0 : 1] ) )
- indexes += OOFormat(@"create index %@_%@ on %@ (%@)\n",
- *tableName, *columnName,
- *tableName, *columnName);
- if ( class_getName( [aClass superclass] )[0] != '_' ) {
- columns += columnName;
- outcols += columnName;
- joinableColumns += columnName;
- }
- }
- free( ivarInfo );
- }
- if ( [recordClass respondsToSelector:@selector(ooTableKey)] )
- createTableSQL += OOFormat( @",\n\tprimary key (%@)",
- *(keyColumns = [recordClass ooTableKey]) );
- if ( [recordClass respondsToSelector:@selector(ooConstraints)] )
- createTableSQL += OOFormat( @",\n\t%@", [recordClass ooConstraints] );
- createTableSQL += "\n)\n";
- if ( [recordClass respondsToSelector:@selector(ooTableSql)] ) {
- createTableSQL = [recordClass ooTableSql];
- indexes = nil;
- }
- tableOfTables->tablesWithNaturalJoin += recordClassName;
- tablesWithNaturalJoin += recordClassName;
- for( Class other in [*metaDataByClass allKeys] ) {
- OOMetaData *otherMetaData = metaDataByClass[other];
- if ( other == recordClass || otherMetaData == tableOfTables )
- continue;
- if ( [self naturalJoinTo:otherMetaData->joinableColumns] > 0 )
- tablesWithNaturalJoin += otherMetaData->recordClassName;
- if ( [otherMetaData naturalJoinTo:joinableColumns] > 0 )
- otherMetaData->tablesWithNaturalJoin += recordClassName;
- }
- return self;
- }
- /**
- Find the columns shared between two classes and that have upper case names (are indexed).
- */
- - (OOStringArray)naturalJoinTo:(cOOStringArray)to {
- //NSLog( @"%@ -- %@", *columns, *to );
- OOStringArray commonColumns = columns & to;
- for ( int i=0 ; i<commonColumns ; i++ )
- if ( islower( (*commonColumns[i])[0] ) )
- ~commonColumns[i--];
- return commonColumns;
- }
- /**
- Encode values ready for insertion into the database (convert OOString to NSString etc.)
- */
- - (cOOValueDictionary)encode:(cOOValueDictionary)values {
- //NSLog( @"%@ - unbox: %@, dates: %@, archived: %@", *values, *unbox, *dates, *archived );
- for ( NSString *key in *unbox ) {
- id value = (id)[*values[key] pointerValue];
- values[key] = value ? value : OONull;
- }
- for ( NSString *key in *dates )
- values[key] = [NSNumber numberWithDouble:[(id)values[key] timeIntervalSince1970]];
- for ( NSString *key in *archived )
- values[key] = (NSValue *)[NSKeyedArchiver archivedDataWithRootObject:values[key]];
- return values;
- }
- /**
- Decode values taken from the database for use in [record setValuesForKeysWithDictionary:values];
- */
- #ifdef OO_ARC
- void ooArcRetain( id value ) {
- // hack to retain value encoded as pointer
- if ( value && value != OONull ) {
- static IMP retainIMP;
- static SEL retainSEL;
- if ( !retainIMP ) {
- retainSEL = sel_getUid( "retain" );
- Method method = class_getInstanceMethod( [value class], retainSEL );
- retainIMP = method_getImplementation( method );
- }
- retainIMP( value, retainSEL );
- }
- }
- #endif
- - (cOOValueDictionary)decode:(cOOValueDictionary)values {
- id value;
- for ( NSString *key in *archived )
- if ( (value = values[key]) )
- values[key] = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)value];
- for ( NSString *key in *dates )
- if ( (value = values[key]) )
- values[key] = [NSDate dateWithTimeIntervalSince1970:[value doubleValue]];
- for ( NSString *key in *boxed ) {
- if ( (value = values[key]) ) {
- value = value != OONull ? OO_RETAIN( value ) : nil;
- #ifdef OO_ARC
- ooArcRetain( value );
- #endif
- OO_RELEASE( values[key] = [[NSValue alloc] initWithBytes:&value objCType:@encode(id)] );
- }
- }
- return values;
- }
- /**
- import values for a list of nodes selected from an XML document.
- */
- + (OOArray<id>)import:(const OOArray<OODictionary<OOString> > &)nodes intoClass:(Class)recordClass {
- OOMetaData *metaData = [self metaDataForClass:recordClass];
- OOArray<id> out;
- for ( NSMutableDictionary *dict in *nodes ) {
- OOStringDictionary node = dict, values;
- for ( NSString *ivar in *metaData->columns )
- values[ivar] = node[ivar];
- id record = [[recordClass alloc] init];
- [record setValuesForKeysWithDictionary:[metaData decode:values]];
- out += record;
- OO_RELEASE( record );
- }
- return out;
- }
- /**
- Convert a string taken from a flat file into record instances which can be inserted into the database.
- */
- + (OOArray<id>)import:(cOOString)string intoClass:(Class)recordClass delimiter:(cOOString)delim {
- OOMetaData *metaData = [self metaDataForClass:recordClass];
- // remove escaped newlines then split by newline
- OOStringArray lines = (string - @"\\\\\n") / @"\n";
- lines--; // pop last empty line
- OOArray<id> out;
- for ( int l=0 ; l<lines ; l++ ) {
- OODictionary<NSString *> values;
- values[metaData->columns] = *lines[l] / delim;
- // empty columns are taken as null values
- for ( NSString *key in *metaData->columns )
- if ( [*values[key] isEqualToString:@""] )
- values[key] = OONull;
- // convert description strings to NSData
- for ( NSString *key in *metaData->blobs )
- OO_RELEASE( values[key] = (NSString *)[[NSData alloc] initWithDescription:values[key]] );
- id record = [[recordClass alloc] init];
- [record setValuesForKeysWithDictionary:[metaData decode:values]];
- out += record;
- OO_RELEASE( record );
- }
- return out;
- }
- /**
- Convert a set of records selected from the database into a string which can be saved to disk.
- */
- + (OOString)export:(const OOArray<id> &)array delimiter:(cOOString)delim {
- OOMetaData *metaData = nil;
- OOString out;
- for ( id record in *array ) {
- if ( !metaData )
- metaData = [record isKindOfClass:[NSDictionary class]] ?
- OONull : [self metaDataForClass:[record class]];
- OODictionary<NSNumber *> values = metaData == OONull ? record :
- *[metaData encode:[record dictionaryWithValuesForKeys:metaData->columns]];
- OOStringArray line;
- NSString *blank = @"";
- for ( NSString *key in *metaData->columns )
- line += *values[key] != OONull ? [values[key] stringValue] : blank;
- out += line/delim+"\n";
- }
- return out;
- }
- /**
- Bind a record to a view containing elements which are to display values from the record.
- The ivar number is selected by the subview's tag value and it's ".text" property if set to
- the value returned record value "stringValue" for the ivar. Supports images stored as
- NSData objects, UISwitches bound to boolean valuea and UITextField for alll other values.
- */
- + (void)bindRecord:(id)record toView:(OOView *)view delegate:(id)delegate {
- #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
- OOMetaData *metaData = [self metaDataForClass:[record class]];
- OOValueDictionary values = [metaData encode:[record dictionaryWithValuesForKeys:metaData->ivars]];
- for ( int i=0 ; i<metaData->ivars ; i++ ) {
- UILabel *label = (UILabel *)[view viewWithTag:1+i];
- id value = values[metaData->ivars[i]];
- if ( [label isKindOfClass:[UIImageView class]] )
- ((UIImageView *)label).image = value != OONull ? [UIImage imageWithData:(NSData *)value] : nil;
- else if ( [label isKindOfClass:[UISwitch class]] ) {
- UISwitch *uiSwitch = (UISwitch *)label;
- uiSwitch.on = value != OONull ? [value charValue] : 0;
- if ( delegate )
- [uiSwitch addTarget:delegate action:@selector(valueChanged:) forControlEvents:UIControlEventValueChanged];
- }
- else if ( [label isKindOfClass:[UIWebView class]] )
- [(UIWebView *)label loadHTMLString:value != OONull ? value : @"" baseURL:nil];
- else if ( label ) {
- label.text = value != OONull ? [value stringValue] : @"";
- if ( [label isKindOfClass:[UITextView class]] ) {
- [(UITextView *)label setContentOffset:CGPointMake(0,0) animated:NO];
- [(UITextView *)label scrollRangeToVisible:NSMakeRange(0,0)];
- }
- }
- if ( [label respondsToSelector:@selector(delegate)] )
- ((UITextField *)label).delegate = delegate;
- label.hidden = NO;
- if ( (label = (UILabel *)[view viewWithTag:-1-i]) ) {
- label.text = **metaData->ivars[i];
- label.hidden = NO;
- }
- }
- OOView *subView;
- for ( int i=metaData->ivars ; (subView = [view viewWithTag:1+i]) ; i++ ) {
- subView.hidden = YES;
- if ( (subView = [view viewWithTag:-1-i]) )
- subView.hidden =YES;
- }
- #endif
- }
- /**
- When the delegate method fires this method should be called to update
- the record with the modified value before updating the database.
- */
- + (void)updateRecord:(id)record fromView:(OOView *)view {
- #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
- if ( view.tag > 0 && [view respondsToSelector:@selector(text)] ) {
- OOMetaData *metaData = [self metaDataForClass:[record class]];
- NSString *name = *metaData->ivars[view.tag-1];
- OOString type = metaData->types[name];
- id value = OO_RETAIN(((UITextField *)view).text );
- if ( type[0] == '{' ) {
- #ifdef OO_ARC
- ooArcRetain( value );
- #endif
- value = [[NSValue alloc] initWithBytes:&value objCType:@encode(id)];
- #ifndef OO_ARC
- OO_RELEASE( (id)[[record valueForKey:name] pointerValue] );
- #endif
- }
- [record setValue:value forKey:name];
- OO_RELEASE( value );
- }
- for ( OOView *subview in [view subviews] )
- [self updateRecord:record fromView:subview];
- #endif
- }
- @end
- @implementation OOView(OOExtras)
- - copyView {
- NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:self];
- OOView *copy = [NSKeyedUnarchiver unarchiveObjectWithData:archived];
- #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
- copy.frame = CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height);
- #else
- copy.frame = NSMakeRect(0.0, 0.0, self.frame.size.width, self.frame.size.height);
- #endif
- return copy;
- }
- @end
- @implementation NSData(OOExtras)
- static int unhex ( unsigned char ch ) {
- return ch >= 'a' ? 10 + ch - 'a' : ch >= 'A' ? 10 + ch - 'A' : ch - '0';
- }
- - initWithDescription:(NSString *)description {
- NSInteger len = [description length]/2, lin = [description lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
- char *bytes = (char *)malloc( len ), *optr = bytes, *hex = (char *)malloc( lin+1 );
- [description getCString:hex maxLength:lin+1 encoding:NSUTF8StringEncoding];
- for ( char *iptr = hex ; *iptr ; iptr+=2 ) {
- if ( *iptr == '<' || *iptr == ' ' || *iptr == '>' )
- iptr--;
- else
- *optr++ = unhex( *iptr )*16 + unhex( *(iptr+1) );
- }
- free( hex );
- return [self initWithBytesNoCopy:bytes length:optr-bytes freeWhenDone:YES];
- }
- - (NSString *)stringValue { return [self description]; }
- @end
- @interface NSString(OOExtras)
- @end
- @implementation NSString(OOExtras)
- - (char)charValue { return [self intValue]; }
- - (char)shortValue { return [self intValue]; }
- - (NSString *)stringValue { return self; }
- @end
- @interface NSArray(OOExtras)
- @end
- @implementation NSArray(OOExtras)
- - (NSString *)stringValue {
- static OOReplace reformat( "/(\\s)\\s+|^\\(|\\)$|\"/$1/" );
- return &([self description] | reformat);
- }
- @end
- @interface NSDictionary(OOExtras)
- @end
- @implementation NSDictionary(OOExtras)
- - (NSString *)stringValue {
- static OOReplace reformat( "/(\\s)\\s+|^\\{|\\}$|\"/$1/" );
- return &([self description] | reformat);
- }
- @end
- #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
- @interface UISwitch(OOExtras)
- @end
- @implementation UISwitch(OOExtras)
- - (NSString *)text { return self.on ? @"1" : @"0"; }
- @end
- #endif