Objective-C++ Preprocessor  5.0 with ARC & GC
C++ classes to enhance syntax and manage reference counting.
objsql.mm
Go to the documentation of this file.
1 /*
2  * objsql.m - implementaion simple persistence layer using objcpp.h
3  * ========
4  *
5  * Created by John Holdsworth on 01/04/2009.
6  * Copyright 2009 John Holdsworth.
7  *
8  * $Id: //depot/ObjCpp/objsql.mm#69 $
9  * $DateTime: 2014/01/09 20:25:53 $
10  *
11  * C++ classes to wrap up XCode classes for operator overload of
12  * useful operations such as access to NSArrays and NSDictionary
13  * by subscript or NSString operators such as + for concatenation.
14  *
15  * This works as the Apple Objective-C compiler supports source
16  * which mixes C++ with objective C. To enable this: for each
17  * source file which will include/import this header file, select
18  * it in Xcode and open it's "Info". To enable mixed compilation,
19  * for the file's "File Type" select: "sourcecode.cpp.objcpp".
20  *
21  * For bugs or ommisions please email objcpp@johnholdsworth.com
22  *
23  * Home page for updates and docs: http://objcpp.johnholdsworth.com
24  *
25  * You may make commercial use of this source in applications without
26  * charge but not sell it as source nor can you remove this notice from
27  * this source if you redistribute. You can make any changes you like
28  * to this code before redistribution but please annotate them below.
29  *
30  * If you find it useful please send a donation via paypal to account
31  * objcpp@johnholdsworth.com. Thanks.
32  *
33  * THIS CODE IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND EITHER
34  * EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
35  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
36  *
37  * IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
38  * WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
39  * THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING
40  * ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT
41  * OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
42  * TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED
43  * BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH
44  * ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN
45  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
46  *
47  */
48 
49 #import <objc/runtime.h>
50 #import <sqlite3.h>
51 
52 #import "objsql.h"
53 
54 #if 0
55 #ifdef OODEBUG
56 #define OODEBUG_SQL 1
57 #endif
58 #endif
59 
61 
62 static NSString *kOOObject = @"__OOOBJECT__", *kOOInsert = @"__ISINSERT__", *kOOUpdate = @"__ISUPDATE__", *kOOExecSQL = @"__OOEXEC__";
63 
64 #pragma mark OORecord abstract superclass for records
65 
66 @implementation OORecord
67 
68 + (id)record OO_AUTORETURNS {
69  return OO_AUTORELEASE( [[self alloc] init] );
70 }
71 
72 + (id)insert OO_AUTORETURNS {
73  OORecord *record = [self record];
74  [record insert];
75  return record;
76 }
77 
78 + (id)insertWithParent:(id)parent {
79  return [[OODatabase sharedInstance] copyJoinKeysFrom:parent to:[self insert]];
80 }
81 
82 - (id)insert { [[OODatabase sharedInstance] insert:self]; return self; }
83 - (id)delete { [[OODatabase sharedInstance] delete:self]; return self; }
84 
85 - (void)update { [[OODatabase sharedInstance] update:self]; }
86 - (void)indate { [[OODatabase sharedInstance] indate:self]; }
87 - (void)upsert { [[OODatabase sharedInstance] upsert:self]; }
88 
89 - (int)commit { return [[OODatabase sharedInstance] commit]; }
91 
92 // to handle null values for ints, floats etc.
93 - (void)setNilValueForKey:(NSString *)key {
94  static OOReference<NSValue *> zeroForNull;
95  if ( !zeroForNull )
96  zeroForNull = [NSNumber numberWithInt:0];
97  [self setValue:zeroForNull forKey:key];
98 }
99 
100 + (OOArray<id>)select {
101  return [[OODatabase sharedInstance] select:nil intoClass:self joinFrom:nil];
102 }
103 
104 + (OOArray<id>)select:(cOOString)sql {
105  return [[OODatabase sharedInstance] select:sql intoClass:self joinFrom:nil];
106 }
107 
108 + (OOArray<id>)selectRecordsRelatedTo:(id)parent {
109  return [[OODatabase sharedInstance] select:nil intoClass:self joinFrom:parent];
110 }
111 
112 - (OOArray<id>)select {
113  return [[OODatabase sharedInstance] select:nil intoClass:[self class] joinFrom:self];
114 }
115 
116 /**
117  import a flat file with column values separated by the delimiter specified into
118  the table associated with this class.
119  */
120 
121 + (int)importFrom:(OOFile &)file delimiter:(cOOString)delim {
122  OOArray<id> rows = [OOMetaData import:file.string() intoClass:self delimiter:delim];
124  return [OODatabase commit];
125 }
126 
127 /**
128  Export a flat file with all rows in the table associated with the record subclass.
129  */
130 
131 + (BOOL)exportTo:(OOFile &)file delimiter:(cOOString)delim {
132  return file.save( [OOMetaData export:[self select] delimiter:delim] );
133 }
134 
135 /**
136  populate a view with the string values taken from the ivars of the record
137  */
138 
139 - (void)bindToView:(OOView *)view delegate:(id)delegate {
140  [OOMetaData bindRecord:self toView:view delegate:delegate];
141 }
142 
143 /**
144  When the delegate method os sent use this method to update the record's values
145  */
146 
147 - (void)updateFromView:(OOView *)view {
148  [OOMetaData updateRecord:self fromView:view];
149 }
150 
151 /**
152  Default description is dictionary containing values of all ivars
153  */
154 
155 - (NSString *)description {
156  OOMetaData *metaData = [OOMetaData metaDataForClass:[self class]];
157  // hack required where record contains a field "description" to avoid recursion
158  OOStringArray ivars; ivars <<= *metaData->ivars; ivars -= "description";
159  return [*[metaData encode:[self dictionaryWithValuesForKeys:ivars]] description];
160 }
161 
162 @end
163 
164 #pragma mark OOAdaptor - all methods required by objsql to access a database
165 
166 /**
167  An internal class representing the interface to a particular database, in this case sqlite3.
168  */
169 
170 @interface OOAdaptor : NSObject {
171  sqlite3 *db;
172  sqlite3_stmt *stmt;
173  struct _str_link {
174  struct _str_link *next; char str[1];
175  } *strs;
177 }
178 
179 - initPath:(cOOString)path database:(OODatabase *)database;
180 - (BOOL)prepare:(cOOString)sql;
181 
182 - (BOOL)bindCols:(cOOStringArray)columns values:(cOOValueDictionary)values startingAt:(int)pno bindNulls:(BOOL)bindNulls;
183 - (OOArray<id>)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(OOMetaData *)metaData;
184 - (sqlite_int64)lastInsertRowID;
185 
186 @end
187 
188 @interface NSData(OOExtras)
189 - initWithDescription:(NSString *)description;
190 @end
191 
192 #pragma mark OODatabase is the low level interface to a particular database
193 
194 @implementation OODatabase
195 
197 
198 /**
199  By default database file is "objsql.db" in the user/application's "Documents" directory
200  and a single shared OODatabase instance used for all db operations.
201  */
202 
204  if ( !sharedInstance )
205  [self sharedInstanceForPath:OODocument("objsql.db").path()];
206  return sharedInstance;
207 }
208 
209 /**
210  Shared instance can be switched between any file paths.
211  */
212 
213  + (OODatabase *)sharedInstanceForPath:(cOOString)path {
214  if ( !!sharedInstance )
216  if ( !!path )
217  OO_RELEASE( sharedInstance = [[OODatabase alloc] initPath:path] );
218  return sharedInstance;
219 }
220 
221 + (BOOL)exec:(cOOString)fmt, ... {
222  va_list argp; va_start(argp, fmt);
223  NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp];
224  va_end( argp );
225  return [[self sharedInstance] exec:OO_AUTORELEASE( sql )];
226 }
227 
228 + (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass joinFrom:(id)parent {
229  return [[self sharedInstance] select:select intoClass:recordClass joinFrom:parent];
230 }
231 + (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass {
232  return [[self sharedInstance] select:select intoClass:recordClass joinFrom:nil];
233 }
234 + (OOArray<id>)select:(cOOString)select {
235  return [[self sharedInstance] select:select intoClass:nil joinFrom:nil];
236 }
237 
238 + (int)insertArray:(const OOArray<id> &)objects { return [[self sharedInstance] insertArray:objects]; }
239 + (int)deleteArray:(const OOArray<id> &)objects { return [[self sharedInstance] deleteArray:objects]; }
240 
241 + (int)insert:(id)object { return [[self sharedInstance] insert:object]; }
242 + (int)delete:(id)object { return [[self sharedInstance] delete:object]; }
243 + (int)update:(id)object { return [[self sharedInstance] update:object]; }
244 
245 + (int)indate:(id)object { return [[self sharedInstance] indate:object]; }
246 + (int)upsert:(id)object { return [[self sharedInstance] upsert:object]; }
247 
248 + (int)commit { return [[self sharedInstance] commit]; }
249 + (int)rollback { return [[self sharedInstance] rollback]; }
251 
252 /**
253  Designated initialiser for OODatabase instances. Generally only the shared instance is used
254  and the OODatabase class object is messaged instead.
255  */
256 
257 - initPath:(cOOString)path {
258  if ( self = [super init] )
259  OO_RELEASE( adaptor = [[OOAdaptor alloc] initPath:path database:self] );
260  return self;
261 }
262 
263 /**
264  Automatically register all classes which are subclasses of a record abstract superclass (e.g. OORecord).
265  */
266 
267 - (OOStringArray)registerSubclassesOf:(Class)recordSuperClass {
268  int numClasses = objc_getClassList( NULL, 0 );
269  Class *classes = (Class *)malloc( sizeof *classes * numClasses );
270  OOArray<Class> viewClasses;
271  OOStringArray classNames;
272 
273  // scan all registered classes for relevant subclasses
274  numClasses = objc_getClassList( classes, numClasses );
275  for ( int c=0 ; c<numClasses ; c++ ) {
276  Class superClass = classes[c];
277  if ( class_getName( superClass )[0] != '_' )
278  while ( (superClass = class_getSuperclass( superClass )) )
279  if ( superClass == recordSuperClass ) {
280  if ( [classes[c] respondsToSelector:@selector(ooTableSql)] )
281  viewClasses += classes[c];
282  else {
284  classNames += class_getName( classes[c] );
285  }
286  break;
287  }
288  }
289 
290  // delay creation views until after tables
291  for ( int c=0 ; c<viewClasses ; c++ ) {
293  classNames += class_getName( viewClasses[c] );
294  }
295 
296  // return classes in order registered
297  free( classes );
298  return classNames;
299 }
300 
301 /**
302  Register a list of classes before using them so OODatabase can determine the relationships between them.
303  */
304 
305 - (void)registerTableClassesNamed:(cOOStringArray)classes {
306  for ( NSString *tableClass in *classes )
307  [self tableMetaDataForClass:[[NSBundle mainBundle] classNamed:tableClass]];
308 }
309 
310 /**
311  Send any SQL to the database. Sql is a format string so escape any '%' characters using '%%'.
312  Any results returned are placed as an array of dictionary values in the database->results.
313  */
314 
315 - (BOOL)exec:(cOOString)fmt, ... {
316  va_list argp; va_start(argp, fmt);
317  NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp];
318  va_end( argp );
319  results = [self select:sql intoClass:NULL joinFrom:nil];
320  OO_RELEASE( sql );
321  return !errcode;
322 }
323 
324 /**
325  Return a single value from row 1, column one from sql sent to the database as a string.
326  */
327 
328 - (OOString)stringForSql:(cOOString)fmt, ... {
329  va_list argp; va_start(argp, fmt);
330  NSString *sql = OO_AUTORELEASE( [[NSString alloc] initWithFormat:fmt arguments:argp] );
331  va_end( argp );
332  if( [self exec:"%@", sql] && results > 0 ) {
333  NSString *aColumnName = [[**results[0] allKeys] objectAtIndex:0];
334  return [(NSNumber *)(*results[0])[aColumnName] stringValue];
335  }
336  else
337  return nil;
338 }
339 
340 /**
341  Used to initialise new child records automatically from parent in relation.
342  */
343 
344 - (id)copyJoinKeysFrom:(id)parent to:(id)newChild {
345  OOMetaData *parentMetaData = [self tableMetaDataForClass:[parent class]],
346  *childMetaData = [self tableMetaDataForClass:[newChild class]];
347  OOStringArray commonColumns = [parentMetaData naturalJoinTo:childMetaData->columns]; ////
348  OOValueDictionary keyValues = [parentMetaData encode:[parent dictionaryWithValuesForKeys:commonColumns]];
349  [newChild setValuesForKeysWithDictionary:[childMetaData decode:keyValues]];
350  return newChild;
351 }
352 
353 /**
354  Build a where clause for the columns specified.
355  */
356 
357 - (OOString)whereClauseFor:(cOOStringArray)columns values:(cOOValueDictionary)values qualifyNulls:(BOOL)qualifyNulls {
358  OOString out;
359  for ( int i=0 ; i<columns ; i++ ) {
360  NSString *name = *columns[i];
361  const char *prefix = i==0 ?"\nwhere":" and";
362 
363  id value = values[name];
364  if ( !value || value == OONull )
365  out += qualifyNulls ? OOFormat( @"%s\n\t%@ is NULL", prefix, name ) : @"";
366  else if ( !qualifyNulls && [value isKindOfClass:[NSString class]] &&
367  [value rangeOfString:@"%"].location != NSNotFound )
368  out += OOFormat( @"%s\n\t%@ LIKE ?", prefix, name );
369  else
370  out += OOFormat( @"%s\n\t%@ = ?", prefix, name );
371  }
372  return out;
373 }
374 
375 /**
376  Prepare the sql passed in adding a where clause with bindings for a join to values taken from the parent record.
377  */
378 
379 - (BOOL)prepareSql:(OOString &)sql joinFrom:(id)parent toTable:(OOMetaData *)metaData {
380  OOValueDictionary joinValues;
381  OOStringArray sharedColumns;
382 
383  if ( parent ) {
384  OOMetaData *parentMetaData = [self tableMetaDataForClass:[parent class]];
385  sharedColumns = [parentMetaData naturalJoinTo:metaData->joinableColumns];
386  joinValues = [parentMetaData encode:[parent dictionaryWithValuesForKeys:sharedColumns]];
387 
388  sql += [self whereClauseFor:sharedColumns values:joinValues qualifyNulls:NO];
389  }
390 
391  if ( [metaData->recordClass respondsToSelector:@selector(ooOrderBy)] )
392  sql += OOFormat( @"\norder by %@", [metaData->recordClass ooOrderBy] );
393 
394 #ifdef OODEBUG_SQL
395  NSLog( @"-[OOMetaData prepareSql:] %@\n%@", *sql, *joinValues );
396 #endif
397 
398  if ( ![*adaptor prepare:sql] )
399  return NO;
400 
401  return !parent || [*adaptor bindCols:sharedColumns values:joinValues startingAt:1 bindNulls:NO];
402 }
403 
404 /**
405  Determine a list of the tables which have a natural join to the record passed in.
406  If the record is a specific instance of from a table this should determine if
407  there are any record which exist using the join.
408  */
409 
410 - (OOArray<OOMetaData *>)tablesRelatedByNaturalJoinFrom:(id)record {
411  OOMetaData *metaData = [record class] == [OOMetaData class] ?
412  record : [self tableMetaDataForClass:[record class]];
413 
414  OOStringArray tablesWithNaturalJoin;
415  tablesWithNaturalJoin <<= metaData->tablesWithNaturalJoin;
416 
417  if ( record && record != metaData )
418  for ( int i=0 ; i<tablesWithNaturalJoin ; i++ ) {
419  OOString sql = OOFormat( @"select count(*) as result from %@", **tablesWithNaturalJoin[i] );
420  OOMetaData *childMetaData = tableMetaDataByClassName[tablesWithNaturalJoin[i]];
421 
422  [self prepareSql:sql joinFrom:record toTable:childMetaData];
423  OOArray<OODictionary<NSNumber *> > tmpResults = [*adaptor bindResultsIntoInstancesOfClass:NULL metaData:nil];
424 
425  if ( ![(*tmpResults[0])["result"] intValue] )
426  ~tablesWithNaturalJoin[i--];
427  }
428 
429  return tableMetaDataByClassName[+tablesWithNaturalJoin];
430 }
431 
432 /**
433  Perform a select from a table on the database using either the sql specified
434  orselect all columns from the table associated with the record class passed in.
435  If a parent is passed in make a natural join from that record.
436  */
437 
438 - (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass joinFrom:(id)parent {
439  OOMetaData *metaData = [self tableMetaDataForClass:recordClass ? recordClass : [parent class]];
440  OOString sql = !select ?
441  OOFormat( @"select %@\nfrom %@", *(metaData->outcols/", "), *metaData->tableName ) : *select;
442 
443  if ( ![self prepareSql:sql joinFrom:parent toTable:metaData] )
444  return nil;
445 
446  return [*adaptor bindResultsIntoInstancesOfClass:recordClass metaData:metaData];
447 }
448 
449 - (OOArray<id>)select:(cOOString)select intoClass:(Class)recordClass {
450  return [self select:select intoClass:recordClass joinFrom:nil];
451 }
452 
453 - (OOArray<id>)select:(cOOString)select {
454  return [self select:select intoClass:nil joinFrom:nil];
455 }
456 
457 /**
458  Returns sqlite3 row identifier for a record instance.
459  */
460 
461 - (long long)rowIDForRecord:(id)record {
462  OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
463  OOString sql = OOFormat( @"select ROWID from %@", *metaData->tableName );
464  OOArray<OODictionary<NSNumber *> > idResults = [self select:sql intoClass:nil joinFrom:record];
465  return [*(*idResults[0])[@"rowid"] longLongValue];
466 }
467 
468 /**
469  Returns sqlite3 row identifier for last inserted record.
470  */
471 
472 - (long long)lastInsertRowID {
473  return [*adaptor lastInsertRowID];
474 }
475 
476 /**
477  Insert an array of record objects into the database. This needs to be commited to take effect.
478  */
479 
480 - (int)insertArray:(const OOArray<id> &)objects {
481  int count = 0;
482  for ( id object in *objects )
483  count = [self insert:object];
484  return count;
485 }
486 
487 /**
488  Delete an array of record objects from the database. This needs to be commited to take effect.
489  */
490 
491 - (int)deleteArray:(const OOArray<id> &)objects {
492  int count = 0;
493  for ( id object in *objects )
494  if ( ![object respondsToSelector:@selector(delete)] )
495  count = [self delete:object];
496  else {
497  [object delete];
498  count++;
499  }
500  return count;
501 }
502 
503 /**
504  Insert the values of the record class instance at the time this method was called into the db.
505  (must be commited to take effect). Returns the total number of outstanding inserts/updates/deletes.
506  */
507 
508 - (int)insert:(id)record {
509  return transaction += OOValueDictionary( kOOObject, record, kOOInsert, kOOInsert, nil );
510 }
511 
512 /**
513  Use the values of the record instance at the time this method is called in a where clause to
514  delete from the database when commit is called. Returns the total number of outstanding
515  inserts/updates/deletes.
516  */
517 
518 - (int)delete:(id)record {
519  return transaction += OOValueDictionary( kOOObject, record, nil );
520 }
521 
522 /**
523  Call this method if you intend to make changes to the record object and save them to the database.
524  This takes a snapshot of the previous values to use as a key for the update operation when "commit"
525  is called. Returns the total number of outstanding inserts/updates/deletes.
526  */
527 
528 - (int)update:(id)record {
529  OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
530  OOValueDictionary oldValues = [metaData encode:[record dictionaryWithValuesForKeys:metaData->columns]];
531  for ( NSString *key in *metaData->tocopy )
532  OO_RELEASE( oldValues[key] = [oldValues[key] copy] );
533  oldValues[kOOUpdate] = (id)kOOUpdate;
534  oldValues[kOOObject] = record;
535  return transaction += oldValues;
536 }
537 
538 /**
539  Inserts a record into the database the deletes any previous record with the same key.
540  This ensures the record's rowid changes if this is used by child records.
541  */
542 
543 - (int)indate:(id)record {
544  OOMetaData *metaData = [self tableMetaDataForClass:[record class]];
545  OOString sql = OOFormat( @"select rowid from %@", *metaData->tableName );
546  OOArray<id> existing = [self select:sql intoClass:nil joinFrom:record];
547  int count = [self insert:record];
548  for ( NSDictionary *exist in *existing ) {
549  OOString sql = OOFormat( @"delete from %@ where rowid = %ld", *metaData->tableName,
550  (long)[[exist objectForKey:@"rowid"] longLongValue] );
551  transaction += OOValueDictionary( kOOExecSQL, *sql, nil );
552  }
553  return count;
554 }
555 
556 /**
557  Inserts a record into the database unless another record with the same key column values
558  exists in which case it will do an update of the previous record (preserving the ROWID.)
559  */
560 
561 - (int)upsert:(id)record {
562  OOArray<id> existing = [self select:nil intoClass:[record class] joinFrom:record];
563  if ( existing > 1 )
564  OOWarn( @"-[ODatabase upsert:] Duplicate record for upsert: %@", record );
565  if ( existing > 0 ) {
566  [self update:existing[0]];
567  (*transaction[-1])[kOOObject] = record;
568  return transaction;
569  }
570  else
571  return [self insert:record];
572 }
573 
574 /**
575  Commit all pending inserts, updates and deletes to the database. Use commitTransaction to perform
576  this inside a database transaction.
577  */
578 
579 - (int)commit {
580  int commited = 0;
581 
582  for ( int i=0 ; i<transaction ; i++ ) {
583  OOValueDictionary values = transaction[i];
584  OOString exec = (NSMutableString *)~values[kOOExecSQL];
585  if ( !!exec ) {
586  if ( ![self exec:@"%@", *exec] )
587  OOWarn( @"-[ODatabase commit] Error in transaction exec: %@ - %s", *exec, errmsg );
588  continue;
589  }
590 
591  OORef<NSObject *> object = *values[kOOObject]; values -= kOOObject;
592  BOOL isInsert = !!~values[kOOInsert], isUpdate = !!~values[kOOUpdate];
593 
594  OOMetaData *metaData = [self tableMetaDataForClass:[object class]];
595  OOValueDictionary newValues = [metaData encode:[object dictionaryWithValuesForKeys:metaData->columns]];
596  OOStringArray changedCols;
597 
598  if ( isUpdate ) {
599  for ( NSString *name in *metaData->columns )
600  if ( ![*newValues[name] isEqual:values[name]] )
601  changedCols += name;
602  }
603  else
604  values = newValues;
605 
606  OOString sql = isInsert ?
607  OOFormat( @"insert into %@ (%@) values (", *metaData->tableName, *(metaData->columns/", ") ) :
608  OOFormat( isUpdate ? @"update %@ set" : @"delete from %@", *metaData->tableName );
609 
610  int nchanged = changedCols;
611  if ( isUpdate && nchanged == 0 ) {
612  OOWarn( @"%s %@ (%@)", errmsg = (char *)"-[ODatabase commit:] Update of unchanged record", *object, *(lastSQL = sql) );
613  continue;
614  }
615 
616  for ( int i=0 ; i<nchanged ; i++ )
617  sql += OOFormat( @"%s\n\t%@ = ?", i==0 ? "" : ",", **changedCols[i] );
618 
619  if ( isInsert ) {
620  OOString quote = "?", commaQuote = ", ?";
621  for ( int i=0 ; i<metaData->columns ; i++ )
622  sql += i==0 ? quote : commaQuote;
623  sql += ")";
624  }
625  else
626  sql += [self whereClauseFor:metaData->columns values:values qualifyNulls:YES];
627 
628 #ifdef OODEBUG_SQL
629  NSLog( @"-[OODatabase commit]: %@ %@", *sql, *values );
630 #endif
631 
632  if ( ![*adaptor prepare:sql] )
633  continue;
634 
635  if ( isUpdate )
636  [*adaptor bindCols:changedCols values:newValues startingAt:1 bindNulls:YES];
637  [*adaptor bindCols:metaData->columns values:values startingAt:1+nchanged bindNulls:isInsert];
638 
639  [*adaptor bindResultsIntoInstancesOfClass:nil metaData:metaData];
640  commited += updateCount;
641  }
642 
643  transaction = nil;
644  return commited;
645 }
646 
647 /**
648  Commit all pending inserts, updates, deletes to the database inside a transaction.
649  */
650 
651 - (int)commitTransaction {
652  [self exec:"BEGIN TRANSACTION"];
653  int updated = [self commit];
654  return [self exec:"COMMIT"] ? updated : 0;
655 }
656 
657 /**
658  Rollback any outstanding inserts, updates, or deletes. Please note updated values
659  are also rolled back inside the actual record in the application as well.
660  */
661 
662 - (int)rollback {
663  for ( NSMutableDictionary *d in *transaction ) {
664  OODictionary<id> values = d;
665 
666  if ( !!~values[kOOUpdate] ) {
667  OORef<OORecord *> record = ~values[kOOObject];
668  OOMetaData *metaData = [self tableMetaDataForClass:[*record class]];
669 
670 #ifndef OO_ARC
671  for ( NSString *name in *metaData->boxed )
672  OO_RELEASE( (id)[[*record valueForKey:name] pointerValue] );
673 #endif
674 
675  [*record setValuesForKeysWithDictionary:[metaData decode:values]];
676  }
677  }
678  return (int)[*~transaction count];
679 }
680 
681 /**
682  Find/create an instance of the OOMetaData class which describes a record class and its associated table.
683  If the table does not exist it will be created along with indexes for columns/ivars which have
684  upper case names for use in joins. Details of the tables parameters can be controlled by using
685  methods in the OOTableCustomisation protool. If it does not exist a meta table class which
686  prepresents OOMetaData records themselves is also created from which the list of all registered
687  tables can be selected.
688  */
689 
690 - (OOMetaData *)tableMetaDataForClass:(Class)recordClass {
691  if ( !recordClass || recordClass == [OOMetaData class] )
692  return [OOMetaData metaDataForClass:[OOMetaData class]];
693 
694  OOString className = class_getName( recordClass );
695  OOMetaData *metaData = tableMetaDataByClassName[className];
696 
697  if ( !metaData ) {
698  metaData = [OOMetaData metaDataForClass:recordClass];
699 
700 #ifdef OODEBUG_SQL
701  NSLog(@"\n%@", *metaData->createTableSQL);
702 #endif
703 
704  if ( metaData->tableName[0] != '_' &&
705  [self stringForSql:"select count(*) from sqlite_master where name = '%@'",
706  *metaData->tableName] == "0" )
707  if ( [self exec:"%@", *metaData->createTableSQL] )
708  for ( NSString *idx in *metaData->indexes )
709  if ( ![self exec:idx] )
710  OOWarn( @"-[OOMetaData tableMetaDataForClass:] Error creating index: %@", idx );
711 
712  tableMetaDataByClassName[className] = metaData;
713  }
714 
715  return metaData;
716 }
717 
718 @end
719 
720 #pragma mark OOAdaptor - implements all access to a particular database
721 
722 @implementation OOAdaptor
723 
724 /**
725  Connect to/create sqlite3 database
726  */
727 
728 - (OOAdaptor *)initPath:(cOOString)path database:(OODatabase *)database {
729  if ( self = [super init] ) {
730  owner = database;
731  OOFile( OOFile( path ).dir() ).mkdir();
732  if ( (owner->errcode = sqlite3_open( path, &db )) != SQLITE_OK ) {
733  OOWarn( @"-[OOAdaptor initPath:database:] Error opening database at path: %@", *path );
734  return nil;
735  }
736  }
737  return self;
738 }
739 
740 /**
741  Prepare a sql statement after which values can be bound and results returned.
742  */
743 
744 - (BOOL)prepare:(cOOString)sql {
745  if ( (owner->errcode = sqlite3_prepare_v2( db, owner->lastSQL = sql, -1, &stmt, 0 )) != SQLITE_OK )
746  OOWarn(@"-[OOAdaptor prepare:] Could not prepare sql: \"%@\" - %s", *owner->lastSQL, owner->errmsg = (char *)sqlite3_errmsg( db ) );
747  return owner->errcode == SQLITE_OK;
748 }
749 
750 - (int)bindValue:(id)value asParameter:(int)pno {
751 #ifdef OODEBUG_BIND
752  NSLog( @"-[OOAdaptor bindValue:bindValue:] bind parameter #%d as: %@", pno, value );
753 #endif
754  if ( !value || value == OONull )
755  return sqlite3_bind_null( stmt, pno );
756 #if OOSQL_THREAD_SAFE_BUT_USES_MORE_MEMORY
757  else if ( [value isKindOfClass:[NSString class]] )
758  return sqlite3_bind_text( stmt, pno, [value UTF8String], -1, SQLITE_STATIC );
759 #else
760  else if ( [value isKindOfClass:[NSString class]] ) {
761  int len = (int)[value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
762  struct _str_link *str = (struct _str_link *)malloc( sizeof *str + len );
763  str->next = strs;
764  strs = str;
765  [value getCString:str->str maxLength:len+1 encoding:NSUTF8StringEncoding];
766  return sqlite3_bind_text( stmt, pno, str->str, len, SQLITE_STATIC );
767  }
768 #endif
769  else if ( [value isKindOfClass:[NSData class]] )
770  return sqlite3_bind_blob( stmt, pno, [value bytes], (int)[value length], SQLITE_STATIC );
771 
772  const char *type = [value objCType];
773  if ( type )
774  switch ( type[0] ) {
775  case 'c': case 's': case 'i': case 'l':
776  case 'C': case 'S': case 'I': case 'L':
777  return sqlite3_bind_int( stmt, pno, [value intValue] );
778  case 'q': case 'Q':
779  return sqlite3_bind_int64( stmt, pno, [value longLongValue] );
780  case 'f': case 'd':
781  return sqlite3_bind_double( stmt, pno, [value doubleValue] );
782  }
783 
784  OOWarn( @"-[OOAdaptor bindValue:bindValue:] Undefined type in bind of parameter #%d: %s, value: %@", pno, type, value );
785  return -1;
786 }
787 
788 /**
789  Bind parameters from a prepared SQL statement. The "objCType" method is used to determine the type to bind.
790  */
791 
792 - (BOOL)bindCols:(cOOStringArray)columns values:(cOOValueDictionary)values startingAt:(int)pno bindNulls:(BOOL)bindNulls {
793  int errcode;
794  for ( NSString *name in *columns )
795  if ( bindNulls || *values[name] != OONull )
796  if ( (errcode = [self bindValue:values[name] asParameter:pno++]) != SQLITE_OK )
797  OOWarn( @"-[OOAdaptor bindCols:...] Bind failed column: %@ - %s (%d)", name, owner->errmsg = (char *)sqlite3_errmsg( db ), owner->errcode = errcode );
798  return owner->errcode == SQLITE_OK;
799 }
800 
801 /**
802  Return a dictionary containing the values for a row returned by the database from a select.
803  These values need to be decoded using a classes metadata to set the ivar values later.
804  */
805 
806 - (OOValueDictionary)valuesForNextRow {
807  int ncols = sqlite3_column_count( stmt );
808  OOValueDictionary values;
809 
810  for ( int i=0 ; i<ncols ; i++ ) {
811  OOString name = sqlite3_column_name( stmt, i );
812  id value = nil;
813 
814  switch ( sqlite3_column_type( stmt, i ) ) {
815  case SQLITE_NULL:
816  value = OONull;
817  break;
818  case SQLITE_INTEGER:
819  value = [[NSNumber alloc] initWithLongLong:sqlite3_column_int64( stmt, i )];
820  break;
821  case SQLITE_FLOAT:
822  value = [[NSNumber alloc] initWithDouble:sqlite3_column_double( stmt, i )];
823  break;
824  case SQLITE_TEXT: {
825  const unsigned char *bytes = sqlite3_column_text( stmt, i );
826  value = [[NSMutableString alloc] initWithBytes:bytes
827  length:sqlite3_column_bytes( stmt, i)
828  encoding:NSUTF8StringEncoding];
829  }
830  break;
831  case SQLITE_BLOB: {
832  const void *bytes = sqlite3_column_blob( stmt, i );
833  value = [[NSData alloc] initWithBytes:bytes length:sqlite3_column_bytes( stmt, i )];
834  }
835  break;
836  default:
837  OOWarn( @"-[OOAdaptor valuesForNextRow:] Invalid type on bind of ivar %@: %d", *name, sqlite3_column_type( stmt, i ) );
838  }
839 
840  values[name] = value;
841  OO_RELEASE( value );
842  }
843 
844  return values;
845 }
846 
847 /**
848  Create instances of the recordClass to store results from a database select or if the record
849  class is not present return a list of dictionary objects with the raw results.
850  */
851 
852 - (OOArray<id>)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(OOMetaData *)metaData {
853  OOArray<id> out;
854  BOOL awakeFromDB = [recordClass instancesRespondToSelector:@selector(awakeFromDB)];
855 
856  while( (owner->errcode = sqlite3_step( stmt )) == SQLITE_ROW ) {
857  OOValueDictionary values = [self valuesForNextRow];
858  if ( recordClass ) {
859  id record = [[recordClass alloc] init];
860  [record setValuesForKeysWithDictionary:[metaData decode:values]];
861 
862  if ( awakeFromDB )
863  [record awakeFromDB];
864 
865  out += record;
866  OO_RELEASE( record );
867  }
868  else
869  out += values;
870  }
871 
872  if ( owner->errcode != SQLITE_DONE )
873  OOWarn(@"-[OOAdaptor bindResultsIntoInstancesOfClass:metaData:] Not done (bind) stmt: %@ - %s", *owner->lastSQL, owner->errmsg = (char *)sqlite3_errmsg( db ) );
874  else {
875  owner->errcode = SQLITE_OK;
876  out.alloc();
877  }
878 
879  while ( strs != NULL ) {
880  struct _str_link *next = strs->next;
881  free( strs );
882  strs = next;
883  }
884  owner->updateCount = sqlite3_changes( db );
885  sqlite3_finalize( stmt );
886  return out;
887 }
888 
889 - (sqlite_int64)lastInsertRowID {
890  return sqlite3_last_insert_rowid( db );
891 }
892 
893 - (void) dealloc {
894  sqlite3_close( db );
895  OO_DEALLOC( super );
896 }
897 
898 @end
899 
900 #pragma mark OOMetaData instances represent a table in the database and it's record class
901 
902 @implementation OOMetaData
903 
904 static OODictionary<OOMetaData *> metaDataByClass;
905 static OOMetaData *tableOfTables;
906 
907 + (NSString *)ooTableTitle { return @"Table MetaData"; }
908 
909 + (OOMetaData *)metaDataForClass:(Class)recordClass OO_RETURNS {
910  if ( !tableOfTables )
911  OO_RELEASE( tableOfTables = [[OOMetaData alloc] initClass:[OOMetaData class]] );
912  OOMetaData *metaData = metaDataByClass[recordClass];
913  if ( !metaData )
914  OO_RELEASE( metaData = [[OOMetaData alloc] initClass:recordClass] );
915  return metaData;
916 }
917 
918 + (OOArray<id>)selectRecordsRelatedTo:(id)record {
920 }
921 
922 - initClass:(Class)aClass {
923  if ( !(self = [super init]) )
924  return self;
925  recordClass = aClass;
926  metaDataByClass[recordClass] = self;
927  recordClassName = class_getName( recordClass );
928  tableTitle = [recordClass respondsToSelector:@selector(ooTableTitle)] ?
929  [recordClass ooTableTitle] : *recordClassName;
930  tableName = [recordClass respondsToSelector:@selector(ooTableName)] ?
931  [recordClass ooTableName] : *recordClassName;
932 
933  if ( aClass == [OOMetaData class] ) {
934  ivars = columns = outcols = boxed = unbox =
935  "tableTitle tableName recordClassName keyColumns ivars columns outcols";
936  return self;
937  }
938 
939  createTableSQL = OOFormat( @"create table %@ (", *tableName );
940 
941  OOArray<Class> hierarchy;
942  do
943  hierarchy += aClass;
944  while ( (aClass = [aClass superclass]) && aClass != [NSObject class] );
945 
946  for ( int h=(int)hierarchy-1 ; h>=0 ; h-- ) {
947  aClass = (Class)hierarchy[h]; ///
948  Ivar *ivarInfo = class_copyIvarList( aClass, NULL );
949  if ( !ivarInfo )
950  continue;
951 
952  for ( int in=0 ; ivarInfo[in] ; in++ ) {
953  OOString columnName = ivar_getName( ivarInfo[in] );
954  ivars += columnName;
955 
956  OOString type = types[columnName] = ivar_getTypeEncoding( ivarInfo[in] ), dbtype = "";
957 
958  SEL columnSel = sel_getUid(columnName);
959  switch ( type[0] ) {
960  case 'c': case 's': case 'i': case 'l':
961  case 'C': case 'S': case 'I': case 'L':
962  case 'q': case 'Q':
963  dbtype = @"int";
964  break;
965  case 'f': case 'd':
966  dbtype = @"real";
967  break;
968  case '{':
969  if( !type["=\"ref\"@\"NS"] )
970  OOWarn( @"-[OOMetaData initClass:] Invalid structure type for ivar %@ in class %@: %@", *columnName, *recordClassName, *type );
971  boxed += columnName;
972  if ( ![recordClass instancesRespondToSelector:columnSel] ) {
973  unbox += columnName;
974  if ( [[recordClass superclass] instancesRespondToSelector:columnSel] )
975  OOWarn( @"-[OOMetaData initClass:] Superclass of class %@ is providing method for column: %@", *recordClassName, *columnName );
976  }
977  case '@':
978  if ( !!type["NS(Mutable)?String\""] )
979  dbtype = @"text";
980  else if ( !!type["\"NSDate\""] ) {
981  dbtype = @"real";
982  dates += columnName;
983  }
984  else {
985  if ( !type["NS(Mutable)?Data\""] )
986  archived += columnName;
987  blobs += columnName;
988  dbtype = @"blob";
989  }
990  break;
991  default:
992  OOWarn( @"-[OOMetaData initClass:] Unknown data type '%@' in class %@", *type, *tableName );
993  archived += columnName;
994  blobs += columnName;
995  dbtype = @"blob";
996  break;
997  }
998 
999  if ( dbtype == @"text" )
1000  tocopy += columnName;
1001 
1002  if ( columnName == @"rowid" || columnName == @"ROWID" ||
1003  columnName == @"OID" || columnName == @"_ROWID_" ) {
1004  outcols += columnName;
1005  continue;
1006  }
1007 
1008  createTableSQL += OOFormat(@"%s\n\t%@ %@ /* %@ */",
1009  !columns?"":",", *columnName, *dbtype, *type );
1010 
1011  if ( iswupper( columnName[columnName[0] != '_' ? 0 : 1] ) )
1012  indexes += OOFormat(@"create index %@_%@ on %@ (%@)\n",
1013  *tableName, *columnName,
1014  *tableName, *columnName);
1015 
1016  if ( class_getName( [aClass superclass] )[0] != '_' ) {
1017  columns += columnName;
1018  outcols += columnName;
1019  joinableColumns += columnName;
1020  }
1021  }
1022 
1023  free( ivarInfo );
1024  }
1025 
1026  if ( [recordClass respondsToSelector:@selector(ooTableKey)] )
1027  createTableSQL += OOFormat( @",\n\tprimary key (%@)",
1028  *(keyColumns = [recordClass ooTableKey]) );
1029 
1030  if ( [recordClass respondsToSelector:@selector(ooConstraints)] )
1031  createTableSQL += OOFormat( @",\n\t%@", [recordClass ooConstraints] );
1032 
1033  createTableSQL += "\n)\n";
1034 
1035  if ( [recordClass respondsToSelector:@selector(ooTableSql)] ) {
1036  createTableSQL = [recordClass ooTableSql];
1037  indexes = nil;
1038  }
1039 
1040  tableOfTables->tablesWithNaturalJoin += recordClassName;
1042 
1043  for( Class other in [*metaDataByClass allKeys] ) {
1044  OOMetaData *otherMetaData = metaDataByClass[other];
1045  if ( other == recordClass || otherMetaData == tableOfTables )
1046  continue;
1047 
1048  if ( [self naturalJoinTo:otherMetaData->joinableColumns] > 0 )
1049  tablesWithNaturalJoin += otherMetaData->recordClassName;
1050  if ( [otherMetaData naturalJoinTo:joinableColumns] > 0 )
1051  otherMetaData->tablesWithNaturalJoin += recordClassName;
1052  }
1053 
1054  return self;
1055 }
1056 
1057 /**
1058  Find the columns shared between two classes and that have upper case names (are indexed).
1059  */
1060 
1061 - (OOStringArray)naturalJoinTo:(cOOStringArray)to {
1062  //NSLog( @"%@ -- %@", *columns, *to );
1063  OOStringArray commonColumns = columns & to;
1064  for ( int i=0 ; i<commonColumns ; i++ )
1065  if ( islower( (*commonColumns[i])[0] ) )
1066  ~commonColumns[i--];
1067  return commonColumns;
1068 }
1069 
1070 /**
1071  Encode values ready for insertion into the database (convert OOString to NSString etc.)
1072  */
1073 
1075  //NSLog( @"%@ - unbox: %@, dates: %@, archived: %@", *values, *unbox, *dates, *archived );
1076  for ( NSString *key in *unbox ) {
1077  id value = (id)[*values[key] pointerValue];
1078  values[key] = value ? value : OONull;
1079  }
1080  for ( NSString *key in *dates )
1081  values[key] = [NSNumber numberWithDouble:[(id)values[key] timeIntervalSince1970]];
1082  for ( NSString *key in *archived )
1083  values[key] = (NSValue *)[NSKeyedArchiver archivedDataWithRootObject:values[key]];
1084  return values;
1085 }
1086 
1087 /**
1088  Decode values taken from the database for use in [record setValuesForKeysWithDictionary:values];
1089  */
1090 
1092  id value;
1093  for ( NSString *key in *archived )
1094  if ( (value = values[key]) )
1095  values[key] = [NSKeyedUnarchiver unarchiveObjectWithData:(NSData *)value];
1096  for ( NSString *key in *dates )
1097  if ( (value = values[key]) )
1098  values[key] = (id)[NSDate dateWithTimeIntervalSince1970:[value doubleValue]];
1099  for ( NSString *key in *boxed ) {
1100  if ( (value = values[key]) ) {
1101  value = value != OONull ? OO_RETAIN( value ) : nil;
1102 #ifdef OO_ARC
1103  if ( value )
1104  CFRetain( OO_BRIDGE(CFTypeRef)value );
1105 #endif
1106  OO_RELEASE( values[key] = [[NSValue alloc] initWithBytes:&value objCType:@encode(id)] );
1107  }
1108  }
1109  return values;
1110 }
1111 
1112 /**
1113  import values for a list of nodes selected from an XML document.
1114  */
1115 
1116 + (OOArray<id>)import:(const OOArray<OODictionary<OOString> > &)nodes intoClass:(Class)recordClass {
1117  OOMetaData *metaData = [self metaDataForClass:recordClass];
1118  OOArray<id> out;
1119 
1120  for ( NSMutableDictionary *dict in *nodes ) {
1121  OOStringDictionary node = dict, values;
1122  for ( NSString *ivar in *metaData->columns )
1123  if ( !!node[ivar] )
1124  values[ivar] = node[ivar];
1125 
1126  id record = [[recordClass alloc] init];
1127  [record setValuesForKeysWithDictionary:[metaData decode:values]];
1128  out += record;
1129  OO_RELEASE( record );
1130  }
1131 
1132  return out;
1133 }
1134 
1135 /**
1136  Convert a string taken from a flat file into record instances which can be inserted into the database.
1137  */
1138 
1139 + (OOArray<id>)import:(cOOString)string intoClass:(Class)recordClass delimiter:(cOOString)delim {
1140  OOMetaData *metaData = [self metaDataForClass:recordClass];
1141  // remove escaped newlines then split by newline
1142  OOStringArray lines = (string - @"\\\\\n") / @"\n";
1143  lines--; // pop last empty line
1144 
1145  OOArray<id> out;
1146  for ( int l=0 ; l<lines ; l++ ) {
1147  OODictionary<NSString *> values;
1148  values[metaData->columns] = *lines[l] / delim;
1149 
1150  // empty columns are taken as null values
1151  for ( NSString *key in *metaData->columns )
1152  if ( [*values[key] isEqualToString:@""] )
1153  values[key] = OONull;
1154 
1155  // convert description strings to NSData
1156  for ( NSString *key in *metaData->blobs )
1157  OO_RELEASE( values[key] = (NSString *)[[NSData alloc] initWithDescription:values[key]] );
1158 
1159  id record = [[recordClass alloc] init];
1160  [record setValuesForKeysWithDictionary:[metaData decode:values]];
1161  out += record;
1162  OO_RELEASE( record );
1163  }
1164 
1165  return out;
1166 }
1167 
1168 /**
1169  Convert a set of records selected from the database into a string which can be saved to disk.
1170  */
1171 
1172 + (OOString)export:(const OOArray<id> &)array delimiter:(cOOString)delim {
1173  OOMetaData *metaData = nil;
1174  OOString out;
1175 
1176  for ( id record in *array ) {
1177  if ( !metaData )
1178  metaData = [record isKindOfClass:[NSDictionary class]] ?
1179  OONull : [self metaDataForClass:[record class]];
1180 
1181  OODictionary<NSNumber *> values = metaData == OONull ? record :
1182  *[metaData encode:[record dictionaryWithValuesForKeys:metaData->columns]];
1183 
1184  OOStringArray line;
1185  NSString *blank = @"";
1186  for ( NSString *key in *metaData->columns )
1187  line += *values[key] != OONull ? [values[key] stringValue] : blank;
1188 
1189  out += line/delim+"\n";
1190  }
1191 
1192  return out;
1193 }
1194 
1195 /**
1196  Bind a record to a view containing elements which are to display values from the record.
1197  The ivar number is selected by the subview's tag value and it's ".text" property if set to
1198  the value returned record value "stringValue" for the ivar. Supports images stored as
1199  NSData objects, UISwitches bound to boolean valuea and UITextField for alll other values.
1200  */
1201 
1202 + (void)bindRecord:(id)record toView:(OOView *)view delegate:(id)delegate {
1203 #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
1204  OOMetaData *metaData = [self metaDataForClass:[record class]];
1205  OOValueDictionary values = [metaData encode:[record dictionaryWithValuesForKeys:metaData->ivars]];
1206 
1207  for ( int i=0 ; i<metaData->ivars ; i++ ) {
1208  UILabel *label = (UILabel *)[view viewWithTag:1+i];
1209  id value = values[metaData->ivars[i]];
1210 
1211  if ( [label isKindOfClass:[UIImageView class]] )
1212  ((UIImageView *)label).image = value != OONull ? [UIImage imageWithData:(NSData *)value] : nil;
1213  else if ( [label isKindOfClass:[UISwitch class]] ) {
1214  UISwitch *uiSwitch = (UISwitch *)label;
1215  uiSwitch.on = value != OONull ? [value charValue] : 0;
1216  if ( delegate )
1217  [uiSwitch addTarget:delegate action:@selector(valueChanged:) forControlEvents:UIControlEventValueChanged];
1218  }
1219  else if ( [label isKindOfClass:[UIWebView class]] )
1220  [(UIWebView *)label loadHTMLString:value != OONull ? value : @"" baseURL:nil];
1221  else if ( label ) {
1222  label.text = value != OONull ? [value stringValue] : @"";
1223  if ( [label isKindOfClass:[UITextView class]] ) {
1224  [(UITextView *)label setContentOffset:CGPointMake(0,0) animated:NO];
1225  [(UITextView *)label scrollRangeToVisible:NSMakeRange(0,0)];
1226  }
1227  }
1228 
1229  if ( [label respondsToSelector:@selector(delegate)] )
1230  ((UITextField *)label).delegate = delegate;
1231  label.hidden = NO;
1232 
1233  if ( (label = (UILabel *)[view viewWithTag:-1-i]) ) {
1234  label.text = **metaData->ivars[i];
1235  label.hidden = NO;
1236  }
1237  }
1238 
1239  OOView *subView;
1240  for ( int i=metaData->ivars ; (subView = [view viewWithTag:1+i]) ; i++ ) {
1241  subView.hidden = YES;
1242  if ( (subView = [view viewWithTag:-1-i]) )
1243  subView.hidden =YES;
1244  }
1245 #endif
1246 }
1247 
1248 /**
1249  When the delegate method fires this method should be called to update
1250  the record with the modified value before updating the database.
1251  */
1252 
1253 + (void)updateRecord:(id)record fromView:(OOView *)view {
1254 #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
1255  if ( view.tag > 0 && [view respondsToSelector:@selector(text)] ) {
1256  OOMetaData *metaData = [self metaDataForClass:[record class]];
1257  NSString *name = *metaData->ivars[view.tag-1];
1258  OOString type = metaData->types[name];
1259  id value = OO_RETAIN(((UITextField *)view).text );
1260 
1261  if ( type[0] == '{' ) {
1262 #ifdef OO_ARC
1263  if ( value )
1264  CFRetain( OO_BRIDGE(CFTypeRef)value );
1265 #endif
1266  value = [[NSValue alloc] initWithBytes:&value objCType:@encode(id)];
1267 #ifndef OO_ARC
1268  OO_RELEASE( (id)[[record valueForKey:name] pointerValue] );
1269 #endif
1270  }
1271 
1272  [record setValue:value forKey:name];
1273  OO_RELEASE( value );
1274  }
1275  for ( OOView *subview in [view subviews] )
1276  [self updateRecord:record fromView:subview];
1277 #endif
1278 }
1279 
1280 @end
1281 
1282 @implementation OOView(OOExtras)
1283 
1285  NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:self];
1286  OOView *copy = [NSKeyedUnarchiver unarchiveObjectWithData:archived];
1287 #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
1288  copy.frame = CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height);
1289 #else
1290  copy.frame = NSMakeRect(0.0, 0.0, self.frame.size.width, self.frame.size.height);
1291 #endif
1292  return copy;
1293 }
1294 
1295 @end
1296 
1297 @implementation NSData(OOExtras)
1298 
1299 static int unhex ( unsigned char ch ) {
1300  return ch >= 'a' ? 10 + ch - 'a' : ch >= 'A' ? 10 + ch - 'A' : ch - '0';
1301 }
1302 
1303 - initWithDescription:(NSString *)description {
1304  NSInteger len = [description length]/2, lin = [description lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
1305  char *bytes = (char *)malloc( len ), *optr = bytes, *hex = (char *)malloc( lin+1 );
1306  [description getCString:hex maxLength:lin+1 encoding:NSUTF8StringEncoding];
1307 
1308  for ( char *iptr = hex ; *iptr ; iptr+=2 ) {
1309  if ( *iptr == '<' || *iptr == ' ' || *iptr == '>' )
1310  iptr--;
1311  else
1312  *optr++ = unhex( *iptr )*16 + unhex( *(iptr+1) );
1313  }
1314 
1315  free( hex );
1316  return [self initWithBytesNoCopy:bytes length:optr-bytes freeWhenDone:YES];
1317 }
1318 
1319 - (NSString *)stringValue { return [self description]; }
1320 
1321 @end
1322 
1323 @interface NSString(OOExtras)
1324 @end
1325 @implementation NSString(OOExtras)
1326 - (char)charValue { return [self intValue]; }
1327 - (char)shortValue { return [self intValue]; }
1328 - (NSString *)stringValue { return self; }
1329 @end
1330 
1331 @interface NSArray(OOExtras)
1332 @end
1333 @implementation NSArray(OOExtras)
1334 - (NSString *)stringValue {
1335  static OOReplace reformat( "/(\\s)\\s+|^\\(|\\)$|\"/$1/" );
1336  return &([self description] | reformat);
1337 }
1338 @end
1339 
1340 @interface NSDictionary(OOExtras)
1341 @end
1342 @implementation NSDictionary(OOExtras)
1343 - (NSString *)stringValue {
1344  static OOReplace reformat( "/(\\s)\\s+|^\\{|\\}$|\"/$1/" );
1345  return &([self description] | reformat);
1346 }
1347 @end
1348 
1349 #ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
1351 @end
1352 @implementation UISwitch(OOExtras)
1353 - (NSString *)text { return self.on ? @"1" : @"0"; }
1354 @end
1355 #endif
1356