/*! @header FTPersistentSet @abstract Module of FT @availability OS X, GNUstep @copyright 2004, 2005, 2006 Free Software Foundation, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  -------------------------------------------------------------------------
  Modification history

  30.03.06 ola     initial version
  23.08.06 ola     license changed
  -------------------------------------------------------------------------
  
*/ #include #include #include #include #include @implementation FTPersistentSetImpl - init { self = [super init]; self->recordNrToObject = nil; self->currentInsertionChunk = nil; self->lock = [[NSLock alloc] init]; self->maxEntriesPerChunk = PS_DEFAULT_MAX_ENTRIES_PER_CHUNCK; return self; } - createDBUsingDataFile: (NSString *) aFilename { BDBDatabaseConfig *dbconfig; if( [[NSFileManager defaultManager] fileExistsAtPath: aFilename] ) { [[FTLogging coreLog] error: @"FTPersistentSetImpl::initAndCreateDBUsingDataFile: "\ "File already exists: %@",aFilename ]; [[[ECAlreadyExistsException alloc] initWithResourceInformation: [NSString stringWithFormat: @"FTPersistentSetImpl::initAndCreateDBUsingDataFile: "\ "File already exists: %@", aFilename ]] raise]; } dbconfig = [self dbConfig]; [dbconfig setAllowCreate: YES]; self->recordNrToObject = [BDBDatabase initWithFilename: aFilename databaseName: nil databaseConfig: dbconfig]; if( [[FTLogging coreLog] isDebugEnabled] ) { [[FTLogging coreLog] debug: @"FTPersistentSetImpl::initAndCreateDBUsingDataFile: Database created!"]; } return self; } - openDBUsingDataFile: (NSString *) aFilename { BDBDatabaseConfig *dbconfig; self = [self init]; if( ![[NSFileManager defaultManager] fileExistsAtPath: aFilename] ) { [[FTLogging coreLog] error: @"FTPersistentSetImpl::initAndSetupDBUsingDataFile: "\ "File does not exist: %@",aFilename ]; [[[ECIllegalStateException alloc] initWithIllegalStateInfo: [NSString stringWithFormat: @"FTPersistentSetImpl::initAndSetupDBUsingDataFile: "\ "File does not exist: %@", aFilename ]] raise]; } dbconfig = [self dbConfig]; [dbconfig setAllowCreate: NO]; self->recordNrToObject = [BDBDatabase initWithFilename: aFilename databaseName: nil databaseConfig: dbconfig]; if( [[FTLogging coreLog] isDebugEnabled] ) { [[FTLogging coreLog] debug: @"FTPersistentSetImpl::initAndSetupDBUsingDataFile: Database opened!"]; } return self; } - (void) closeDB { [self->recordNrToObject close]; [self->recordNrToObject release]; self->recordNrToObject = nil; [self->currentInsertionChunk release]; self->currentInsertionChunk = nil; } - (void) dealloc { if( nil != self->recordNrToObject ) { [self->recordNrToObject close]; [self->recordNrToObject release]; } if( nil != self->currentInsertionChunk ) { [self->currentInsertionChunk store]; [self->currentInsertionChunk release]; } [self->lock release]; [super dealloc]; } - addObject: (id) anObject { if( [[FTLogging coreLog] isTraceEnabled] ) { [[FTLogging coreLog] trace: @"FTPersistentSetImpl::addObject:..." ]; } [self validateDBIsOpen]; if( (![anObject conformsToProtocol: @protocol(NSObject)]) || (![anObject conformsToProtocol:@protocol(NSCoding)]) ) { [[FTLogging coreLog] critical: @"FTPersistentSetImpl::adObject: Given instance "\ "does not conform to NSObject or NSCoding. Object=%@", anObject]; [[[ECIllegalArgumentException alloc] initWithArgumentInfo: @"FTPersistentSetImpl::adObject: Given instance "\ "does not conform to NSObject or NSCoding."] raise]; } [self->lock lock]; NS_DURING if( nil == self->currentInsertionChunk ) { // no chunk available -> read/create one: [self newChunk]; } else { if( self->maxEntriesPerChunk <= [self->currentInsertionChunk count] ) { // no free space left in current chunk [self newChunk]; } } if( nil == self->currentInsertionChunk ) { [[FTLogging coreLog] fatal: @"FTPersistentSetImpl::addObject: Unable to get free chunk!"]; [[[ECIllegalStateException alloc] initWithIllegalStateInfo: @"FTPersistentSetImpl::addObject: Unable to get free chunk!"] raise]; } [self->currentInsertionChunk addObject: anObject]; /* easy solution without transactions so far: */ [_FTPersistentSetTransactionStep addTransactionStepForChunk: self->currentInsertionChunk forSet: self]; NS_HANDLER [self->lock unlock]; [localException raise]; NS_ENDHANDLER [self->lock unlock]; return self; } - (_FTPersistentSetChunk *) chunkAtRecordNumber: (unsigned) recNr { return [_FTPersistentSetChunk readFromDatabase: self->recordNrToObject atRecordNumber: recNr]; } - (_FTPersistentSetChunk *) chunkContainingObject: (id) anObject { id chunkIterator; chunkIterator = [self chunkIterator]; while( [chunkIterator hasNext] ) { _FTPersistentSetChunk *currentChunk = [chunkIterator next]; if( [currentChunk containsObject: anObject] ) { return currentChunk; } } return nil; } - (BOOL) containsObject: (id) anObject { id chunk; [self validateDBIsOpen]; [self->lock lock]; NS_DURING chunk = [self chunkContainingObject: anObject]; NS_HANDLER [self->lock unlock]; [localException raise]; NS_ENDHANDLER [self->lock unlock]; return nil != chunk; } - (id ) chunkIterator { return [[_FTPersistentSetChunkIterator alloc] initWithPersistentSet: self]; } - (unsigned) count { unsigned nr_elements = 0; [self validateDBIsOpen]; [self->lock lock]; NS_DURING id iterator = [self chunkIterator]; while( [iterator hasNext] ) { _FTPersistentSetChunk *current = [iterator next]; nr_elements += [current count]; } NS_HANDLER [self->lock unlock]; [localException raise]; NS_ENDHANDLER [self->lock unlock]; return nr_elements; } - (BDBDatabaseConfig *) dbConfig { BDBDatabaseConfig * dbConfig = [[BDBDatabaseConfig alloc] init]; [dbConfig setDatabaseType: BDB_BTREE]; [dbConfig setBTreeRecordNumbering: YES]; [dbConfig setAllowDuplicates: NO]; return dbConfig; } - (id ) iterator { id toReturn; [self validateDBIsOpen]; [self->lock lock]; NS_DURING toReturn = [[_FTPersistentSetIterator alloc] initWithPersistentSet: self]; NS_HANDLER [self->lock unlock]; [localException raise]; NS_ENDHANDLER [self->lock unlock]; return toReturn; } - removeObject: (id) anObject { _FTPersistentSetChunk *chunk; [self validateDBIsOpen]; [self->lock lock]; NS_DURING chunk = [self chunkContainingObject: anObject]; if( nil != chunk ) { [chunk removeObject: anObject]; [_FTPersistentSetTransactionStep addTransactionStepForChunk: chunk forSet: self]; } NS_HANDLER [self->lock unlock]; [localException raise]; NS_ENDHANDLER [self->lock unlock]; return self; } - newChunk { unsigned current_rec_nr, highest_rec_nr; BOOL found; unsigned start_rec_nr; _FTPersistentSetChunk *foundChunk; // examine currently used bdb record number: if( nil != self->currentInsertionChunk ) { [self->currentInsertionChunk store]; current_rec_nr = [self->currentInsertionChunk recordNumber]; [self->currentInsertionChunk release]; self->currentInsertionChunk = nil; } else { current_rec_nr = 1; } // now find an available record number. Todo: the approach implemented here // could be a bit better: EC_AUTORELEASEPOOL_BEGIN NS_DURING found = NO; highest_rec_nr = current_rec_nr; start_rec_nr = current_rec_nr; while( !found ) { if( [[FTLogging coreLog] isDebugEnabled] ) { [[FTLogging coreLog] debug: @"FTPersistentSetImpl::newChunk: Looking for free chunk at %u...", current_rec_nr]; } foundChunk = [self chunkAtRecordNumber: current_rec_nr]; if( nil != foundChunk ) { // does this chunk have free space? if( self->maxEntriesPerChunk < [foundChunk count] ) { found = YES; self->currentInsertionChunk = [foundChunk retain]; break; } else { /* current record has also no availabe space: try next one */ current_rec_nr++; } } else { /* number of avaiable records exceeded. Start with the beginning: */ current_rec_nr = 1; } if( current_rec_nr > highest_rec_nr ) { highest_rec_nr = current_rec_nr; } /* have we reached the record number from where we have started the search? * If YES, then there is no free chunk and we have to add a new one. */ if( start_rec_nr == current_rec_nr ) { /* create new chunk: */ self->currentInsertionChunk = [_FTPersistentSetChunk createForDatabase: self->recordNrToObject atRecordNumber: highest_rec_nr]; found = YES; break; } } NS_HANDLER [[FTLogging coreLog] fatal: @"FTPersistentSetImpl::newChunk: Got exception while examining new "\ "chunk. Exception = :%@", localException]; self->currentInsertionChunk = nil; [localException raise]; NS_ENDHANDLER EC_AUTORELEASEPOOL_END return self; } - (void) validateDBIsOpen { if( nil == self->recordNrToObject ) { [[FTLogging coreLog] error: @"FTPersistentSetImpl::validateDBIsOpen: DB is not mounted!"]; [[[ECIllegalStateException alloc] initWithIllegalStateInfo: @"FTPersistentSetImpl::validateDBIsOpen: DB is not mounted"] raise]; } } @end /************************************************** _FTPersistentSetChunk */ @implementation _FTPersistentSetChunk + (_FTPersistentSetChunk *) createForDatabase: (BDBDatabase *) aDatabase atRecordNumber: (unsigned) a_record_nr { _FTPersistentSetChunk *result; result = [[_FTPersistentSetChunk alloc] initForDatabase: aDatabase atRecordNumber: a_record_nr usingObjects: nil]; result->isModified = YES; [result store]; return result; } + (_FTPersistentSetChunk *) readFromDatabase: (BDBDatabase *) aDatabase atRecordNumber: (unsigned) record_nr { _FTPersistentSetChunk *result = nil; /* try to access daatabase record first: */ NS_DURING BDBDatabaseRecordNumber *recNo; BDBDatabaseEntry *value; BDBOperationStatus opStatus; recNo = [[[BDBDatabaseRecordNumber alloc] initWithEntryNumber: record_nr] autorelease]; value = [[[BDBDatabaseEntry alloc] init] autorelease]; opStatus = [aDatabase getEntryWithTransaction: nil recordNumber: recNo data: value]; if( BDB_STATUS_SUCCESS == opStatus ) { result = [[_FTPersistentSetChunk alloc] initForDatabase: aDatabase atRecordNumber: record_nr usingObjects: [value object]]; } else { if( BDB_STATUS_NOTFOUND != opStatus ) { /* an unexpected error occured */ [[[ECIllegalStateException alloc] initWithIllegalStateInfo: [NSString stringWithFormat: @"_FTPersistentSetChunk::initForDatabase: Unable to read chunk"\ " from database record nr=%u", record_nr]] raise]; } } NS_HANDLER [[FTLogging coreLog] error: @"_FTPersistentSetChunk::readFromDatabase: Error while reading"\ " chunk at record %u from database. Exception: %@", record_nr, localException]; NS_ENDHANDLER return result; } - initForDatabase: (BDBDatabase *) aDatabase atRecordNumber: (unsigned) a_record_nr usingObjects: (NSArray *) givenObjects { self = [super init]; self->database = [aDatabase retain]; self->bdb_record_nr = a_record_nr; self->chunkInTransaction = NO; self->lock = [[NSLock alloc] init]; if( nil != givenObjects ) { self->objects = [givenObjects retain]; } else { self->objects = [[NSMutableArray alloc] init]; } return self; } - addObject: (id) anObject { if( [[FTLogging coreLog] isTraceEnabled] ) { [[FTLogging coreLog] trace: @"_FTPersistentSetChunk::addObject: Adding object \"%@\" to "\ "chunk \"%@\"", anObject, self]; } [self->objects addObject: anObject]; self->isModified = YES; return self; } - (BOOL) containsObject: (id) anObject { return [self->objects containsObject: anObject]; } - (unsigned) count { return [self->objects count]; } - (BOOL) isChunkInTransaction { return self->chunkInTransaction; } - (NSArray *) objects { return [NSArray arrayWithArray: self->objects]; } - loadChunk { EC_AUTORELEASEPOOL_BEGIN NS_DURING BDBDatabaseRecordNumber *recNo; BDBDatabaseEntry *value; BDBOperationStatus opStatus; recNo = [[[BDBDatabaseRecordNumber alloc] initWithEntryNumber: self->bdb_record_nr] autorelease]; value = [[[BDBDatabaseEntry alloc] init] autorelease]; opStatus = [database getEntryWithTransaction: nil recordNumber: recNo data: value]; if( BDB_STATUS_SUCCESS != opStatus ) { [[[ECIllegalStateException alloc] initWithIllegalStateInfo: [NSString stringWithFormat: @"_FTPersistentSetChunk::initForDatabase: Unable to read chunk"\ " from database record nr=%u", self->bdb_record_nr]] raise]; } if( nil != value ) { self->objects = [[value object] retain]; } else { self->objects = [[NSMutableArray alloc] init]; } NS_HANDLER [[FTLogging coreLog] fatal: @"_FTPersistentSetChunk::initForDatabase: Unable to read chunk"\ " from database record nr=%u", self->bdb_record_nr]; [localException raise]; NS_ENDHANDLER EC_AUTORELEASEPOOL_END return self; } - (void) dealloc { [self->objects release]; [self->database release]; [self->lock release]; [super dealloc]; } - (unsigned) recordNumber { return self->bdb_record_nr; } - (void) remove { BDBDatabaseRecordNumber *recNo; BDBOperationStatus opStatus= BDB_STATUS_UNKNOWN; recNo = [[[BDBDatabaseRecordNumber alloc] initWithEntryNumber: self->bdb_record_nr] autorelease]; NS_DURING opStatus = [self->database deleteEntryWithTransaction: nil key: recNo]; if( BDB_STATUS_SUCCESS != opStatus ) { [[[ECIllegalStateException alloc] initWithIllegalStateInfo: [NSString stringWithFormat: @"_FTPersistentSetChunk::remove: Unable to remove chunk"\ " from database at record nr=%u", self->bdb_record_nr]] raise]; } NS_HANDLER [[FTLogging coreLog] fatal: @"_FTPersistentSetChunk::remove: Unable to remove chunk"\ " from database at record nr=%u. BDB error code=%u", self->bdb_record_nr, opStatus ]; [localException raise]; NS_ENDHANDLER self->isModified = NO; } - removeObject: (id) anObject { [self->objects removeObject: anObject]; self->isModified = YES; return self; } - setChunkInTransaction: (BOOL) itIs { [self->lock lock]; if( NO == self->chunkInTransaction ) { self->chunkInTransaction = YES; } [self->lock unlock]; return self; } - store { BDBDatabaseRecordNumber *recNo; BDBDatabaseEntry *value; BDBOperationStatus opStatus; if( !self->isModified ) { return self; } recNo = [[[BDBDatabaseRecordNumber alloc] initWithEntryNumber: self->bdb_record_nr] autorelease]; value = [[[BDBDatabaseEntry alloc] initWithObject: self->objects] autorelease]; NS_DURING if( [[FTLogging coreLog] isDebugEnabled] ) { [[FTLogging coreLog] debug: @"_FTPersistentSetChunk::store: Writing %u number of entries", [self->objects count]]; } opStatus = [self->database putEntryWithTransaction: nil key: recNo value: value]; if( BDB_STATUS_SUCCESS != opStatus ) { [[[ECIllegalStateException alloc] initWithIllegalStateInfo: [NSString stringWithFormat: @"_FTPersistentSetChunk::store: Unable to write chunk"\ " to database at record nr=%u", self->bdb_record_nr]] raise]; } NS_HANDLER [[FTLogging coreLog] fatal: @"_FTPersistentSetChunk::store: Unable to write chunk"\ " to database at record nr=%u", self->bdb_record_nr]; [localException raise]; NS_ENDHANDLER self->isModified = NO; return self; } - (void) update { if( 0 < [self->objects count] ) { [self store]; } else { [self remove]; } } @end /* ----------------------------------------- _FTPersistentSetChunkIterator */ @implementation _FTPersistentSetChunkIterator - initWithPersistentSet: (FTPersistentSetImpl *) aPersistentSet { self = [super init]; self->current_rec_number = 1; self->persistentSet = [aPersistentSet retain]; self->fetchedBefore = NO; self->fetchedChunk = nil; return self; } - (void) dealloc { [self->persistentSet release]; [self->fetchedChunk release]; [super dealloc]; } - (BOOL) hasNext { if( !self->fetchedBefore ) { [self fetchChunk]; self->fetchedBefore = YES; } return (nil != self->fetchedChunk); } - next { if( !self->fetchedBefore ) { [self fetchChunk]; self->fetchedBefore = YES; } self->fetchedBefore = NO; if( nil != self->fetchedChunk ) { self->current_rec_number++; } return self->fetchedChunk; } - fetchChunk { if( nil != self->fetchedChunk ) { [self->fetchedChunk release]; self->fetchedChunk = nil; } NS_DURING self->fetchedChunk = [[self->persistentSet chunkAtRecordNumber: self->current_rec_number] retain]; NS_HANDLER [[FTLogging coreLog] fatal: @"_FTPersistentSetChunkIterator::fetchChunk: Unable to read "\ "chunk from database record nr=%u", self->current_rec_number]; [localException raise]; NS_ENDHANDLER return self; } @end /* ----------------------------------------- _FTPersistentSetIterator */ @implementation _FTPersistentSetIterator - initWithPersistentSet: (FTPersistentSetImpl *) aPersistentSet { self = [super init]; self->chunkIterator = [[aPersistentSet chunkIterator] retain]; self->chunkElements = nil; self->elementFetched = NO; self->nextObject = nil; return self; } - (void) dealloc { [self->chunkIterator release]; [self->chunkElements release]; [self->nextObject release]; [super dealloc]; } - fetchNextChunkElements { _FTPersistentSetChunk *nextChunk = [self->chunkIterator next]; if( nil == nextChunk ) { self->chunkElements = nil; self->nextObject = nil; return self; } self->chunkElements = [[[nextChunk objects] objectEnumerator] retain]; return self; } - (BOOL) hasNext { return nil != [self currentElement]; } - currentElement { if( self->elementFetched ) { /* current element has already been loaded */ return self->nextObject; } if( nil != self->chunkElements ) { /* read current element as next element of the current enumeration */ self->nextObject = [[self->chunkElements nextObject] retain]; if( nil != self->nextObject ) { self->elementFetched = YES; return self->nextObject; } else { /* no more elements in current enumeration --> prepare reading * enumeration of next chunk */ [self->chunkElements release]; self->chunkElements = nil; } } [self fetchNextChunkElements]; self->elementFetched = YES; if( nil != self->chunkElements ) { self->nextObject = [[self->chunkElements nextObject] retain]; } else { self->nextObject = nil; } return self->nextObject; } - next { id toReturn; toReturn = [[self currentElement] retain]; if( nil != toReturn ) { /** remove object marked as "current" from internal state since it is not * the "current" object any more after this call */ [self->nextObject release]; self->nextObject = nil; self->elementFetched = NO; } return toReturn; } @end /* ------------------------------------ _FTPersistentSetTransactionStep */ static NSLock *_lockPersistentSetTransactionStep = nil; /* refers to the chunk instance which has to be updated */ #define _TX_CONTEXT_CHUNK @"__chunkRef" @implementation _FTPersistentSetTransactionStep + (void) initialze { if( self == [_FTPersistentSetTransactionStep class] ) { _lockPersistentSetTransactionStep = [[NSLock alloc] init]; } } + (void) addTransactionStepForChunk: (_FTPersistentSetChunk *) aChunk forSet: (FTPersistentSetImpl *) aSet { [_lockPersistentSetTransactionStep lock]; NS_DURING /* create an identifier unique to the given chunk: */ NSString *transactionStepId = [NSString stringWithFormat: @"%u/%u", [aSet hash], [aChunk hash]]; FTTransactionImpl *currentTransaction; FTTransactionContext *context; id transactionStep; /* if a transaction for this chunk does not exist in the current * transaction then add it...: */ currentTransaction = [[FTSessionImpl currentSession] currentTransaction]; transactionStep = [currentTransaction transactionStepForKey: transactionStepId]; if( nil == transactionStep ) { /* transaction does not exist for the given chunk. */ context = [currentTransaction createContext]; transactionStep = [[_FTPersistentSetTransactionStep alloc] initWithChunk: aChunk forContext: context ]; [currentTransaction addTransactionStep: transactionStep withContext: context identifiedByKey: transactionStepId]; [context release]; [transactionStep release]; } NS_HANDLER [_lockPersistentSetTransactionStep unlock]; [localException raise]; NS_ENDHANDLER [_lockPersistentSetTransactionStep unlock]; } - initWithChunk: (_FTPersistentSetChunk *) chunk forContext: (FTTransactionContext *) aContext { self = [super init]; [aContext addObject: chunk forKey: _TX_CONTEXT_CHUNK]; return self; } - (BOOL) performAction: (FTTransactionContext *) transactionContext { _FTPersistentSetChunk *chunk = (_FTPersistentSetChunk *) [transactionContext objectForKey: _TX_CONTEXT_CHUNK]; if( [[FTLogging coreLog] isDebugEnabled] ) { [[FTLogging coreLog] debug : @"_FTPersistentSetTransactionStep::"\ "performAction: Using chunk = %@", chunk]; } if( nil == chunk ) { [[[ECIllegalStateException alloc] initWithIllegalStateInfo: @"_FTPersistentSetTransactionStep::"\ "performAction: No chunk in current context!"] raise ]; } [chunk update]; return YES; } - (BOOL) undoAction: (FTTransactionContext *) transactionContext { [[[ECNotImplementedException alloc] initWithOperationInformation: @"_FTPersistentSetTransactionStep::undoAction" ] raise]; return NO; } @end