|
|
Sample Programs from NeXSTEP 3.3
/* TableDataSource.m
*
* This data source reads a flat file table (such as Product.table or Item.table)
* and generates EOGeneric records that can be passed to an EOController.
*
* You may freely copy, distribute, and reuse the code in this example.
* NeXT disclaims any warranty of any kind, expressed or implied, as to its
* fitness for any particular use.
*
*
*
*/
#import <appkit/appkit.h>
#import <eoaccess/eoaccess.h>
#import "TableDataSource.h"
#import "TableDataSourcePrivate.h"
#import "DetailTableDataSource.h"
#include <sys/file.h>
#define @QUALIFIER_ALL @"*"
#define @QPROPERTY @"PROPERTY"
#define @QVALUE @"VALUE"
@implementation TableDataSource
// init a new TableDataSource with an array of EOGeneric records and the path to
// the table file (the flat file database!). The entity name will be found from a
// record in the array.
// Use this initialization method when you have existing records (possibly from
// a real database) and you want to save them to a file
- initWithEOGenericRecords:(NSArray *)records tablePath:(NSString *)tablePath {
NSString *tableFile;
NSMutableArray *dictArray = [NSMutableArray array];
NSDictionary *dict;
int i;
// We need at least a record to get the entity
if (![records count]) {
NSLog(@"<initWithEOGenericRecords:tablePath:> No records");
return nil;
}
entity = [[records objectAtIndex:0] entity];
tableFile=[NSString stringWithFormat:@"%@/%@.table", tablePath, [entity name]];
for (i=0; i<[records count]; i++) {
dict = [[records objectAtIndex:i] valuesForKeys:[entity classPropertyNames]];
[dictArray addObject:dict];
}
// Save the records to a file as a property-list string
if (![[[dictArray description] dataUsingEncoding:NSASCIIStringEncoding]
writeToFile:tableFile atomically:NO]) {
NSLog(@"<initWithEOGenericRecords:tablePath:> DataSource %@ failed to save", [entity name]);
return nil;
}
// Call the designated initialization method
else return [self initWithEntity:entity tablePath:tablePath];
}
// The designated initialization method for TableDataSource
// Pick an entity in the model, then initialize the source from a flat file that
// contains a string representation of an array of dictionaries.
// Note that unlike the EODatabaseSataSource, this dataSource has a "state".
// It cashes the eos (dictionaries) that it read from the file and also maintains
// a few hash tables to speed up fetch operations...
// Although the state of the TableDataSource is an array of dictionaries (I call it
// the snapshot) the TabledataSource will copy these dictionaries into an array of
// EOGeneric records before handing them to a controller...
- initWithEntity:(EOEntity *)anEntity tablePath:(NSString *)tablePath {
NSString *primaryKey;
NSArray *relations;
NSMutableArray *qualifiers = [NSMutableArray array];
[super init];
entity = [anEntity retain];
primaryKey = [(EOAttribute *)[[entity primaryKeyAttributes] objectAtIndex:0] name];
relations = [entity relationships];
detailSources = [[NSMutableArray array] retain];
return [self initWithTable:tablePath primaryKey:primaryKey qualifierKeys:qualifiers];
}
// The new NSObject world, I guess
- (void)dealloc {
[table autorelease];
[eos autorelease];
[lookupTables autorelease];
[qualifier autorelease];
[uniqueKey autorelease];
[detailSources autorelease];
[entity autorelease];
[orderByKey autorelease];
[super dealloc];
}
// The path to persistent storage, here to the "table" file...
- (NSString *)tablePath {
return table;
}
// The other one...
- setTablePath:(NSString *)aPath {
[table autorelease];
table = [aPath retain];
return self;
}
// The supported entity, we got it from the EOmodel at initialization time
- (EOEntity *)entity {
return entity;
}
// A custom way (meaning different from the EOF way) to qualify the source for a given property name and value
// The equivalent SQL is: WHERE( key = value)
- qualifyForProperty:(NSString *)key andValue:value {
NSMutableDictionary *qual = [NSMutableDictionary dictionary];
if (![[self keys] containsObject:key]) {
NSLog(@"%@ is not a valid property of entity %@", key, [entity name]);
qual=nil;
}
else {
[qual setObject:key forKey:@QPROPERTY];
[qual setObject:value forKey:@QVALUE];
}
return [self setQualifier:qual];
}
// Qualify for all records
- setEntityQualifier {
NSMutableDictionary *qual =[NSMutableDictionary dictionary];
[qual setObject:uniqueKey forKey:@QPROPERTY];
[qual setObject:@QUALIFIER_ALL forKey:@QVALUE];
return [self setQualifier:qual];
}
// Set the qualifier before the fetch
- setQualifier:(NSMutableDictionary *)newQualifier {
[qualifier autorelease];
if (newQualifier) qualifier = [newQualifier retain];
else qualifier=nil;
//NSLog(@"New qualifier %@", [(NSMutableDictionary *)qualifier description]);
return self;
}
- setEmptySetQualifier {
[qualifier autorelease];
qualifier=nil;
return self;
}
- (BOOL)orderBy:(NSString *)key {
if (![[self keys] containsObject:key]) return NO;
else {
[orderByKey autorelease];
orderByKey = [key retain];
[eos sortUsingFunction:eoSort context:orderByKey];
return YES;
}
}
- setOrderDescendantSources:(BOOL)aFlag {
orderDescendantSources=aFlag;
return self;
}
// ************************************* DATASOURCE PROTOCOL ****************/
// The class property names (keys for the dictionary or EOGenericRecord)
- (NSArray *)keys {
NSArray *attrs = [entity attributes];
NSMutableArray *keys = [NSMutableArray array];
int i;
for (i=0; i<[attrs count]; i++) {
EOAttribute *at = [attrs objectAtIndex:i];
[keys addObject:[at name]];
}
return keys;
}
- createObject{
NSNumber *newId=[self findNextPrimaryKey];
NSMutableDictionary *newObject=[[NSMutableDictionary alloc] init];
if (!newId) return nil;
else {
NSArray *allKeys = [self keys];
NSArray *relations = [entity relationships];
int i, count;
for (i=0, count=[allKeys count]; i<count; i++) {
id key = [allKeys objectAtIndex:i];
[newObject setObject:@"" forKey:key];
}
for (i=0, count=[relations count]; i<count; i++) {
EORelationship *rel = [relations objectAtIndex:i];
if ([rel isToMany]) [newObject setObject:[newId description] forKey:[rel name]];
}
[newObject setObject:[newId description] forKey:uniqueKey];
return newObject;
}
}
- (BOOL)insertObject:object {
NSNumber *objectId=[object objectForKey:uniqueKey];
NSArray *allKeys;
int i;
NSMutableDictionary *newObject;
if (![self isValidNewPrimaryKey:objectId]) {
NSLog(@"DataSource cannot insert; primary key is invalid: %@", [(NSString *)objectId description]);
return NO;
}
newObject=[NSMutableDictionary dictionary];
allKeys = [self keys];
for (i=0; i<[allKeys count]; i++) {
id key = [allKeys objectAtIndex:i];
id value = [object objectForKey:key];
if ([value isEqual:[EONull null]]) value=@"";
[newObject setObject:value forKey:key];
}
[eos addObject:newObject];
if (orderByKey) [eos sortUsingFunction:eoSort context:orderByKey];
[self modifyLookupTables];
return YES;
}
- (BOOL)deleteAllObjects {
int i;
for (i=[eos count]-1; i>=0; i--) {
[eos removeObject:[eos objectAtIndex:i]];
}
[self modifyLookupTables];
return YES;
}
- (BOOL)deleteObject:object {
id primaryKeyValue = [object objectForKey:uniqueKey];
NSMutableArray *deletes=[[lookupTables objectForKey:uniqueKey] objectForKey:primaryKeyValue];
int i;
if ( !deletes || ![deletes count]) return NO;
for (i=0; i<[deletes count]; i++) {
[eos removeObject:[deletes objectAtIndex:i]];
}
[self modifyLookupTables];
return YES;
}
- (BOOL)updateObject:object {
id primaryKeyValue = [object objectForKey:uniqueKey];
NSMutableArray *update=[[lookupTables objectForKey:uniqueKey] objectForKey:primaryKeyValue];
NSArray *allKeys;
NSMutableDictionary *eo;
int i;
NSString *key;
id value;
if (!update || [update count]!=1) return NO;
allKeys = [self keys];
eo = [update objectAtIndex:0];
for (i=0; i<[allKeys count]; i++) {
key = [allKeys objectAtIndex:i];
value = [object objectForKey:key];
if (!value || [value isEqual:[EONull null]] ) value=@"";
[eo setObject:value forKey:key];
}
if (orderByKey) [eos sortUsingFunction:eoSort context:orderByKey];
[self modifyLookupTables];
return YES;
}
- (NSArray *)fetchObjects {
NSArray *records;
NSString *qualifierPropertyKey = [qualifier objectForKey:@QPROPERTY];
NSString *qualifierPropertyValue = [qualifier objectForKey:@QVALUE];
if (qualifierPropertyKey && qualifierPropertyValue) {
if ( [qualifierPropertyValue isEqual:@QUALIFIER_ALL] ) {
records =[self eosArrayToGenericRecordsArray:eos];
}
else {
NSMutableDictionary *hash = [lookupTables objectForKey:qualifierPropertyKey];
if (!hash) {
NSLog(@"No lookup table for qualifier key %@", qualifierPropertyKey);
records =nil;
}
else {
NSArray *qualifiedEos=[hash objectForKey:qualifierPropertyValue];
if (!qualifiedEos || ![qualifiedEos count]) {
//NSLog(@"No qualified eos where %@=%@", qualifierPropertyKey, [(NSString *)qualifierPropertyValue description]);
records =nil;
}
records =[self eosArrayToGenericRecordsArray:qualifiedEos];
}
}
}
else {
//NSLog(@"nil or uncomplete qualifier %@", [(NSMutableDictionary *)qualifier description]);
records = [NSArray array];
}
//NSLog(@"TABLE %@ - FETCHED OBJECTS: %d", [entity name], [records count]);
return records;
}
// This is where we save the eos to persistent storage (COMMIT in SQL)
- (BOOL)saveObjects{
NSString *tableFile=[NSString stringWithFormat:@"%@/%@.table", table, [entity name]];
if (![[[eos description] dataUsingEncoding:NSASCIIStringEncoding]
writeToFile:tableFile atomically:NO]) {
NSString *emess;
if ( (!access([tableFile cString], F_OK)) && (access([tableFile cString], W_OK)) ) {
int ans;
emess = [NSString stringWithFormat:@"\'%@\' data source cannot save changes. You do not have write permission on \'%@\'. Do you want to overwrite?", [entity name], tableFile];
ans=NXRunAlertPanel("ERROR", [emess cString], "Overwrite", "Cancel", NULL);
if (ans==NX_ALERTDEFAULT) return [self forceSaveObjects];
else return NO;
}
else return NO;
}
else return YES;
}
- (BOOL)canDelete {
return YES;
}
// Not implemented yet...
// Coerce a value to the appropriate type.
// This method should convert to either an NSNumber, NSString, NSData,
// a custom type, or nil. The value return by this method may be safely
// passed to an EO via takeValuesFromDictionary:. This method is used
// by controllers to coerce values supplied from associations before
// those values are passed on to the EOs.
- coerceValue: value forKey: (NSString *)key {
// sorry, another day , maybe...
return value;
}
// ****************************** MASTER DATASOURCE PROTOCOL ****************/
// What we are doing here is closer to a master-peer than a master-detail setup.
// We are creating the detail dataSource and handing it to the association.
// This new detail dataSource will be attached to the detail controller automatically
// From there, the detail controller and its DetailTableDataSource will be on their own
// Also notice than in an EOF master-detail setup, a master eo (meaning an eo
// produced by the master source) "carries" its detail eos. In other word if one
// writes [masterEo objectForKey:masterDetailRelationshipKey], the master eo will
// return an array full of detail eos (assuming that the master detail relationship
// was set as a class property name in EOModeler). Although it could, the TableDataSource
// does not support that feature...
- (id <EOQualifiableDataSources>)dataSourceQualifiedByKey:(NSString *)key {
EORelationship *rel = [entity relationshipNamed:key];
EOEntity *detailEntity=[rel destinationEntity];
DetailTableDataSource *detailSource;
if (!rel) return nil;
if (!detailEntity) return nil;
detailSource = [[(DetailTableDataSource *)[DetailTableDataSource alloc] initWithMasterDataSource:self entity:detailEntity] autorelease];
if (orderByKey && orderDescendantSources) [detailSource orderBy:orderByKey];
// We cash it (I had some grandiose plane, unused today!!)
[detailSources addObject:detailSource];
return detailSource;
}
// ***************************************************************************/
// a convenience method to fetch all the objects in the entity
- (NSArray *)fetchAllObjects {
NSMutableDictionary *qual =[NSMutableDictionary dictionary];
[qual setObject:uniqueKey forKey:@QPROPERTY];
[qual setObject:@QUALIFIER_ALL forKey:@QVALUE];
[self setQualifier:qual];
return [self fetchObjects];
}
// a convenience fetch method
- objectForPrimaryKey:value {
id hash = [lookupTables objectForKey:uniqueKey];
NSArray *objects = [hash objectForKey:value];
id object;
EOGenericRecord *record;
NSMutableDictionary *primaryKeyDictionary;
NSString *key;
id copy;
int count, j;
NSArray *allKeys = [self keys];
if ([objects count]!=1) return nil;
object = [objects objectAtIndex:0];
primaryKeyDictionary=[NSMutableDictionary dictionary];
[primaryKeyDictionary setObject:value forKey:uniqueKey];
record = [[EOGenericRecord alloc] initWithPrimaryKey:primaryKeyDictionary
entity:entity];
for (j=0, count=[allKeys count]; j<count; j++) {
key = [allKeys objectAtIndex:j];
copy = [[object objectForKey:key] copy];
[record setObject:[copy autorelease] forKey:key];
}
return [record autorelease];
}
- addLookupTableForKey:(NSString *)key {
NSMutableDictionary *hash;
hash = [self createLookupTableForQualifierKey:key];
[lookupTables setObject:hash forKey:key];
return self;
}
@end
// ************************************* Private Category ****************/
@implementation TableDataSource (Private)
// This initialization method is called by initWithEntity:tablePath:
// It sets the key for the primary key and creates a key->array of objects hash
// tables for each element of the qualifiers array.
- initWithTable:(NSString *)tablePath primaryKey:(NSString *)primaryKey qualifierKeys:(NSArray *)qualifiers {
// Create snapshot from table (eos)
table = [tablePath retain];
if (![self createSnapshot]) return nil;
// Create a dictionary of hash tables for query (key is like @"ID", value is a lookup table)
uniqueKey = [primaryKey retain];
if ( !([self createLookupTables:qualifiers]) ) return nil;
// set qualifier to select everything
// To be clean, we should have a qualifier Class, oh well...it is a rush job
// after all
orderByKey=nil;
orderDescendantSources = NO;
qualifier = [[NSMutableDictionary alloc] init];
[qualifier setObject:uniqueKey forKey:@QPROPERTY];
[qualifier setObject:@QUALIFIER_ALL forKey:@QVALUE];
return self;
}
// The flat file is read here and the objects that have been retrieved are cashed
// in an instance variable (of class NSMutableArray) called eos
- createSnapshot {
NSString *tableFile=[NSString stringWithFormat:@"%@/%@", table, [entity externalName]];
NSString *tableString;
NSArray *nonMutableEos;
// read the table file that should be in a property list format
// Isn't foundation great or what?!!!
if ( !(tableString=[[NSString alloc] initWithContentsOfFile:tableFile]) ||
!(nonMutableEos = [tableString propertyList]) ) {
NSLog(@"<createSnapshot> Cannot init from table %@", tableFile);
return nil;
}
// Make deep mutable copy
else {
NSEnumerator *eoEnumerator = [nonMutableEos objectEnumerator];
NSDictionary *nonMutableEo;
eos = [[NSMutableArray alloc] init];
while ( nonMutableEo = [eoEnumerator nextObject] ) {
[eos addObject: [[nonMutableEo mutableCopy] autorelease]];
}
}
return self;
}
// Create the lookup tables (to speed up fetch operations)
// The qualifiers array contains the property names that we should hash on
// a lookup table is in the form key = "a property key" --> value = an array of
// dictionaries...
- createLookupTables:(NSArray *)qualifiers {
int i, count;
NSMutableDictionary *hash;
NSString *qualifierKey;
lookupTables = [[NSMutableDictionary alloc] init];
hash = [self createLookupTableForQualifierKey:uniqueKey];
[lookupTables setObject:hash forKey:uniqueKey];
for (i=0, count=[qualifiers count]; i<count; i++) {
qualifierKey = [qualifiers objectAtIndex:i];
if ( ![lookupTables objectForKey:qualifierKey] ) {
//NSLog(@"Creating lookup table for qualifierKey %@", qualifierKey);
if ( !(hash = [self createLookupTableForQualifierKey:qualifierKey]) ) return nil;
[lookupTables setObject:hash forKey:qualifierKey];
}
}
return self;
}
// Create a lookup table for a given property name (the key)
- createLookupTableForQualifierKey:(NSString *)key {
NSMutableDictionary *hash;
int i, count;
NSDictionary *eo;
NSMutableArray *values;
id value;
hash=[NSMutableDictionary dictionary];
for (i=0, count=[eos count]; i<count; i++) {
eo = [eos objectAtIndex:i];
// get the property value for the qualifier key
if ( !(value = [eo objectForKey:key]) ) {
NSLog(@"<createLookupTableForQualifierKey:> eos dictionary does not respond to qualifier key %@", key);
return nil;
}
// It is the first time that we encounter an eo such that propertyValue(key) = value
if ( !(values = [hash objectForKey:value]) ) {
values = [NSMutableArray array];
[values addObject:eo];
[hash setObject:values forKey:[(NSObject *)value description]];
}
// We have already found an eo such that propertyValue(key) = value
else {
[values addObject:eo];
}
}
//NSLog(@"HASH FOR KEY %@\n%@", key, [(NSDictionary *)hash description]);
return hash;
}
// After insert, delete or update opeations, the lookup tables must be refreshed
- modifyLookupTables {
NSArray *allKeys=[lookupTables allKeys];
int i, count;
NSString *key;
NSMutableDictionary *hash;
for (i=0, count=[allKeys count]; i<count; i++) {
key = [allKeys objectAtIndex:i];
[lookupTables removeObjectForKey:key];
hash = [self createLookupTableForQualifierKey:key];
[lookupTables setObject:hash forKey:key];
}
return self;
}
// a method to transform an array of eos (state of the TableDataSource) into
// an array of EOGeneric records that we can hand to the outside worls...
- (NSMutableArray *)eosArrayToGenericRecordsArray:(NSArray *)eoArray {
NSMutableArray *records = [NSMutableArray array];
int i, eoCount;
for (i=0, eoCount=[eoArray count]; i<eoCount; i++) {
NSMutableDictionary *eo;
NSMutableDictionary *primaryKey=[NSMutableDictionary dictionary];
EOGenericRecord *record;
NSArray *allKeys;
NSArray *relations;
int j, count;
eo = [eoArray objectAtIndex:i];
allKeys = [self keys];
[primaryKey setObject:[eo objectForKey:uniqueKey] forKey:uniqueKey];
record = [[[EOGenericRecord alloc] initWithPrimaryKey:primaryKey entity:entity] autorelease];
for (j=0, count=[allKeys count]; j<count; j++) {
NSString *key = [allKeys objectAtIndex:j];
id copy;
copy = [[eo objectForKey:key] copy];
[record setObject:[copy autorelease] forKey:key];
}
relations = [entity relationships];
// We support master-details but not flattened attribute for now!!!
for (j=0, count=[relations count]; j<count; j++) {
EORelationship *rel = [relations objectAtIndex:j];
id copy;
if ([rel isToMany]) {
copy = [[eo objectForKey:uniqueKey] copy];
[record setObject:[copy autorelease] forKey:[rel name]];
}
}
[records addObject:record];
}
return records;
}
// Unlike the EODatabaseDataSource, when a TableDataSource creates a new object
// It sets the primary key value for the created object.
// This method computes the next available primary key (the next in the sequence)
- (NSNumber *)findNextPrimaryKey {
int i, max=-1;
for (i=0; i<[eos count]; i++) {
id eo;
int current;
eo = [eos objectAtIndex:i];
current = [[eo objectForKey:uniqueKey] intValue];
if (current > max) max=current;
}
return [NSNumber numberWithInt:max+1];
}
// make sure the the primary key of a new object handed for insertion has a valid primary key
// (in this case, valid means not used by a record stored in the table)
- (BOOL)isValidNewPrimaryKey:(NSNumber *)number {
int i;
for (i=0; i<[eos count]; i++) {
id eo;
eo = [eos objectAtIndex:i];
if ([number isEqual:[eo objectForKey:uniqueKey]]) return NO;
}
return YES;
}
- (BOOL)forceSaveObjects {
NSString *tableFile=[NSString stringWithFormat:@"%@/%@.table", table, [entity name]];
NSString *emess=@"Cannot overwrite; changes will not be saved...";
NSString *cmd;
cmd = [NSString stringWithFormat:@"mv %@ %@.otto", tableFile, tableFile];
if (system([cmd cString])) {
NXRunAlertPanel("ERROR", [emess cString], "OK", NULL, NULL);
return NO;
}
cmd = [NSString stringWithFormat:@"cp %@.otto %@; ", tableFile, tableFile];
if (system([cmd cString])) {
NXRunAlertPanel("ERROR", [emess cString], "OK", NULL, NULL);
return NO;
}
cmd = [NSString stringWithFormat:@"rm -f %@.otto; chmod u+w %@", tableFile, tableFile];
system([cmd cString]);
if (![[[eos description] dataUsingEncoding:NSASCIIStringEncoding]
writeToFile:tableFile atomically:NO]) return NO;
else return YES;
}
int eoSort(id eo1, id eo2, void *context) {
NSString *key=(NSString *)context;
NSString *val1, *val2;
if (!key) return NSOrderedSame;
val1 = [eo1 objectForKey:key];
val2 = [eo2 objectForKey:key];
if (!val1 || !val2) return NSOrderedSame;
else return [val1 compare:val2];
}
@end
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.