/*!
@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