/* Copyright (C) 2004 SKYRIX Software AG This file is part of OpenGroupware.org. OGo 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, or (at your option) any later version. OGo 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 OGo; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // $Id$ #include "OCSFolder.h" #include "OCSFolderManager.h" #include "OCSFolderType.h" #include "OCSChannelManager.h" #include "OCSFieldExtractor.h" #include "NSURL+OCS.h" #include "EOAdaptorChannel+OCS.h" #include "EOQualifier+OCS.h" #include "OCSStringFormatter.h" #include "common.h" @implementation OCSFolder static BOOL debugOn = NO; static BOOL doLogStore = NO; static Class NSStringClass = Nil; static Class NSNumberClass = Nil; static Class NSCalendarDateClass = Nil; static OCSStringFormatter *stringFormatter = nil; + (void)initialize { NSUserDefaults *ud = [NSUserDefaults standardUserDefaults]; debugOn = [ud boolForKey:@"OCSFolderDebugEnabled"]; doLogStore = [ud boolForKey:@"OCSFolderStoreDebugEnabled"]; NSStringClass = [NSString class]; NSNumberClass = [NSNumber class]; NSCalendarDateClass = [NSCalendarDate class]; stringFormatter = [OCSStringFormatter sharedFormatter]; } - (id)initWithPath:(NSString *)_path primaryKey:(id)_folderId folderTypeName:(NSString *)_ftname folderType:(OCSFolderType *)_ftype location:(NSURL *)_loc quickLocation:(NSURL *)_qloc folderManager:(OCSFolderManager *)_fm { if ((self = [super init])) { self->folderManager = [_fm retain]; self->folderInfo = [_ftype retain]; self->folderId = [_folderId copy]; self->folderName = [[_path lastPathComponent] copy]; self->path = [_path copy]; self->location = [_loc retain]; self->quickLocation = [_qloc retain]; self->folderTypeName = [_ftname copy]; } return self; } - (id)init { return [self initWithPath:nil primaryKey:nil folderTypeName:nil folderType:nil location:nil quickLocation:nil folderManager:nil]; } - (void)dealloc { [self->folderManager release]; [self->folderInfo release]; [self->folderId release]; [self->folderName release]; [self->path release]; [self->location release]; [self->quickLocation release]; [self->folderTypeName release]; [super dealloc]; } /* accessors */ - (NSNumber *)folderId { return self->folderId; } - (NSString *)folderName { return self->folderName; } - (NSString *)path { return self->path; } - (NSURL *)location { return self->location; } - (NSURL *)quickLocation { return self->quickLocation; } - (NSString *)folderTypeName { return self->folderTypeName; } - (OCSFolderManager *)folderManager { return self->folderManager; } - (OCSChannelManager *)channelManager { return [[self folderManager] channelManager]; } - (NSString *)storeTableName { return [[self location] ocsTableName]; } - (NSString *)quickTableName { return [[self quickLocation] ocsTableName]; } /* channels */ - (EOAdaptorChannel *)acquireStoreChannel { return [[self channelManager] acquireOpenChannelForURL:[self location]]; } - (EOAdaptorChannel *)acquireQuickChannel { return [[self channelManager] acquireOpenChannelForURL:[self quickLocation]]; } - (void)releaseChannel:(EOAdaptorChannel *)_channel { [[self channelManager] releaseChannel:_channel]; if (debugOn) [self debugWithFormat:@"released channel: %@", _channel]; } - (BOOL)canConnectStore { return [[self channelManager] canConnect:[self location]]; } - (BOOL)canConnectQuick { return [[self channelManager] canConnect:[self quickLocation]]; } /* operations */ - (NSArray *)subFolderNames { return [[self folderManager] listSubFoldersAtPath:[self path] recursive:NO]; } - (NSArray *)allSubFolderNames { return [[self folderManager] listSubFoldersAtPath:[self path] recursive:YES]; } - (id)_fetchValueOfColumn:(NSString *)_col attributeName:(NSString *)_attrName inContentWithName:(NSString *)_name { EOAdaptorChannel *channel; NSException *error; NSDictionary *row; NSArray *attrs; NSString *result; NSString *sql; if ((channel = [self acquireStoreChannel]) == nil) { [self logWithFormat:@"ERROR(%s): could not open storage channel!", __PRETTY_FUNCTION__]; return nil; } /* generate SQL */ sql = @"SELECT "; sql = [sql stringByAppendingString:_col]; sql = [sql stringByAppendingString:@" FROM "]; sql = [sql stringByAppendingString:[self storeTableName]]; sql = [sql stringByAppendingString:@" WHERE \"c_name\" = '"]; sql = [sql stringByAppendingString:_name]; sql = [sql stringByAppendingString:@"'"]; /* run SQL */ if ((error = [channel evaluateExpressionX:sql]) != nil) { [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@", __PRETTY_FUNCTION__, sql, error]; [self releaseChannel:channel]; return nil; } /* fetch results */ result = nil; attrs = [channel describeResults]; if ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) { result = [[[row objectForKey:_attrName] copy] autorelease]; if (![result isNotNull]) result = nil; [channel cancelFetch]; } /* release and return result */ [self releaseChannel:channel]; return result; } - (NSNumber *)versionOfContentWithName:(NSString *)_name { return [self _fetchValueOfColumn:@"c_version" attributeName:@"cVersion" inContentWithName:_name]; } - (NSString *)fetchContentWithName:(NSString *)_name { return [self _fetchValueOfColumn:@"c_content" attributeName:@"cContent" inContentWithName:_name]; } - (NSDictionary *)fetchContentsOfAllFiles { /* Note: try to avoid the use of this method! The key of the dictionary will be filename, the value the content. */ NSMutableDictionary *result; EOAdaptorChannel *channel; NSException *error; NSDictionary *row; NSArray *attrs; NSString *sql; if ((channel = [self acquireStoreChannel]) == nil) { [self logWithFormat:@"ERROR(%s): could not open storage channel!", __PRETTY_FUNCTION__]; return nil; } /* generate SQL */ sql = @"SELECT \"c_name\", \"c_content\" FROM "; sql = [sql stringByAppendingString:[self storeTableName]]; /* run SQL */ if ((error = [channel evaluateExpressionX:sql]) != nil) { [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@", __PRETTY_FUNCTION__, sql, error]; [self releaseChannel:channel]; return nil; } /* fetch results */ result = [NSMutableDictionary dictionaryWithCapacity:128]; attrs = [channel describeResults]; while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) { NSString *cName, *cContent; cName = [row objectForKey:@"cName"]; cContent = [row objectForKey:@"cContent"]; if (![cName isNotNull]) { [self logWithFormat:@"ERROR: missing cName in row: %@", row]; continue; } if (![cContent isNotNull]) { [self logWithFormat:@"ERROR: missing cContent in row: %@", row]; continue; } [result setObject:cContent forKey:cName]; } /* release and return result */ [self releaseChannel:channel]; return result; } /* writing content */ - (NSString *)_formatRowValue:(id)_value { if (![_value isNotNull]) return @"NULL"; if ([_value isKindOfClass:NSStringClass]) return [stringFormatter stringByFormattingString:_value]; if ([_value isKindOfClass:NSNumberClass]) return [_value stringValue]; if ([_value isKindOfClass:NSCalendarDateClass]) { /* be smart ... convert to timestamp */ return [NSString stringWithFormat:@"%i", [_value timeIntervalSince1970]]; } [self logWithFormat:@"cannot handle value class: %@", [_value class]]; return nil; } - (NSString *)_generateInsertStatementForRow:(NSDictionary *)_row tableName:(NSString *)_table { // TODO: move to NSDictionary category? NSMutableString *sql; NSArray *keys; unsigned i, count; if (_row == nil || _table == nil) return nil; keys = [_row allKeys]; sql = [NSMutableString stringWithCapacity:512]; [sql appendString:@"INSERT INTO "]; [sql appendString:_table]; [sql appendString:@" ("]; for (i = 0, count = [keys count]; i < count; i++) { if (i != 0) [sql appendString:@", "]; [sql appendString:[keys objectAtIndex:i]]; } [sql appendString:@") VALUES ("]; for (i = 0, count = [keys count]; i < count; i++) { id value; if (i != 0) [sql appendString:@", "]; value = [_row objectForKey:[keys objectAtIndex:i]]; value = [self _formatRowValue:value]; [sql appendString:value]; } [sql appendString:@")"]; return sql; } - (NSString *)_generateUpdateStatementForRow:(NSDictionary *)_row tableName:(NSString *)_table whereColumn:(NSString *)_colname isEqualTo:(id)_value { // TODO: move to NSDictionary category? NSMutableString *sql; NSArray *keys; unsigned i, count; if (_row == nil || _table == nil) return nil; keys = [_row allKeys]; sql = [NSMutableString stringWithCapacity:512]; [sql appendString:@"UPDATE "]; [sql appendString:_table]; [sql appendString:@" SET "]; for (i = 0, count = [keys count]; i < count; i++) { id value; value = [_row objectForKey:[keys objectAtIndex:i]]; value = [self _formatRowValue:value]; if (i != 0) [sql appendString:@", "]; [sql appendString:[keys objectAtIndex:i]]; [sql appendString:@" = "]; [sql appendString:value]; } [sql appendString:@" WHERE "]; [sql appendString:_colname]; [sql appendString:@" = "]; [sql appendString:[self _formatRowValue:_value]]; return sql; } - (NSException *)writeContent:(NSString *)_content toName:(NSString *)_name { EOAdaptorChannel *storeChannel, *quickChannel; NSMutableDictionary *quickRow, *contentRow; OCSFieldExtractor *extractor; NSException *error; NSNumber *storedVersion; BOOL isNewRecord; NSCalendarDate *nowDate; NSNumber *now; NSString *qsql, *bsql; /* check preconditions */ if (_name == nil) { return [NSException exceptionWithName:@"OCSStoreException" reason:@"no content filename was provided" userInfo:nil]; } if (_content == nil) { return [NSException exceptionWithName:@"OCSStoreException" reason:@"no content was provided" userInfo:nil]; } /* run */ error = nil; nowDate = [NSCalendarDate date]; now = [NSNumber numberWithUnsignedInt:[nowDate timeIntervalSince1970]]; if (doLogStore) [self logWithFormat:@"should store content: '%@'\n%@", _name, _content]; storedVersion = [self versionOfContentWithName:_name]; if (doLogStore) [self logWithFormat:@" version: %@", storedVersion]; isNewRecord = [storedVersion isNotNull] ? NO : YES; /* extract quick info */ extractor = [self->folderInfo quickExtractor]; quickRow = [extractor extractQuickFieldsFromContent:_content]; [quickRow setObject:_name forKey:@"c_name"]; if (doLogStore) [self logWithFormat:@" store quick: %@", quickRow]; /* make content row */ contentRow = [NSMutableDictionary dictionaryWithCapacity:16]; [contentRow setObject:_name forKey:@"c_name"]; if (isNewRecord) [contentRow setObject:now forKey:@"c_creationdate"]; [contentRow setObject:now forKey:@"c_lastmodified"]; if (isNewRecord) [contentRow setObject:[NSNumber numberWithInt:0] forKey:@"c_version"]; else { [contentRow setObject:[NSNumber numberWithInt:[storedVersion intValue]] forKey:@"c_version"]; } [contentRow setObject:_content forKey:@"c_content"]; /* open channels */ if ((storeChannel = [self acquireStoreChannel]) == nil) { [self logWithFormat:@"ERROR(%s): could not open storage channel!"]; return nil; } if ((quickChannel = [self acquireQuickChannel]) == nil) { [self logWithFormat:@"ERROR(%s): could not open quick channel!"]; [self releaseChannel:storeChannel]; return nil; } // TODO: gen SQL, execute in transactions if (isNewRecord) { /* insert */ qsql = [self _generateInsertStatementForRow:quickRow tableName:[self quickTableName]]; bsql = [self _generateInsertStatementForRow:contentRow tableName:[self storeTableName]]; if ((error = [storeChannel evaluateExpressionX:bsql]) != nil) { [self logWithFormat:@"ERROR(%s): cannot insert content '%@': %@", __PRETTY_FUNCTION__, bsql, error]; } else if ((error = [quickChannel evaluateExpressionX:qsql]) != nil) { NSString *delsql; NSException *delErr; [self logWithFormat:@"ERROR(%s): cannot insert quick '%@': %@", __PRETTY_FUNCTION__, qsql, error]; delsql = [@"DELETE FROM " stringByAppendingString:[self storeTableName]]; delsql = [delsql stringByAppendingString:@" WHERE c_name="]; delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]]; if ((delErr = [storeChannel evaluateExpressionX:delsql]) != nil) { [self logWithFormat: @"ERROR(%s): cannot delete content '%@' after quick-fail: %@", __PRETTY_FUNCTION__, delsql, error]; } } } else { /* update */ qsql = [self _generateUpdateStatementForRow:quickRow tableName:[self quickTableName] whereColumn:@"c_name" isEqualTo:_name]; bsql = [self _generateUpdateStatementForRow:contentRow tableName:[self storeTableName] whereColumn:@"c_name" isEqualTo:_name]; if ((error = [storeChannel evaluateExpressionX:bsql]) != nil) { [self logWithFormat:@"ERROR(%s): cannot update content '%@': %@", __PRETTY_FUNCTION__, bsql, error]; } else if ((error = [quickChannel evaluateExpressionX:qsql]) != nil) { [self logWithFormat:@"ERROR(%s): cannot update quick '%@': %@", __PRETTY_FUNCTION__, qsql, error]; } } [self releaseChannel:storeChannel]; [self releaseChannel:quickChannel]; return error; } - (NSException *)deleteContentWithName:(NSString *)_name { EOAdaptorChannel *storeChannel, *quickChannel; NSException *error; NSString *delsql; /* check preconditions */ if (_name == nil) { return [NSException exceptionWithName:@"OCSDeleteException" reason:@"no content filename was provided" userInfo:nil]; } if (doLogStore) [self logWithFormat:@"should delete content: '%@'", _name]; /* open channels */ if ((storeChannel = [self acquireStoreChannel]) == nil) { [self logWithFormat:@"ERROR(%s): could not open storage channel!"]; return nil; } if ((quickChannel = [self acquireQuickChannel]) == nil) { [self logWithFormat:@"ERROR(%s): could not open quick channel!"]; [self releaseChannel:storeChannel]; return nil; } /* delete rows */ delsql = [@"DELETE FROM " stringByAppendingString:[self storeTableName]]; delsql = [delsql stringByAppendingString:@" WHERE c_name="]; delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]]; if ((error = [storeChannel evaluateExpressionX:delsql]) != nil) { [self logWithFormat: @"ERROR(%s): cannot delete content '%@': %@", __PRETTY_FUNCTION__, delsql, error]; } else { /* content row deleted, now delete the quick row */ delsql = [@"DELETE FROM " stringByAppendingString:[self quickTableName]]; delsql = [delsql stringByAppendingString:@" WHERE c_name="]; delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]]; if ((error = [quickChannel evaluateExpressionX:delsql]) != nil) { [self logWithFormat: @"ERROR(%s): cannot delete quick row '%@': %@", __PRETTY_FUNCTION__, delsql, error]; /* Note: we now have a "broken" record, needs to be periodically GCed by a script! */ } } /* release channels and return */ [self releaseChannel:storeChannel]; [self releaseChannel:quickChannel]; return error; } - (NSString *)columnNameForFieldName:(NSString *)_fieldName { return _fieldName; } /* SQL generation */ - (NSString *)generateSQLForSortOrderings:(NSArray *)_so { NSMutableString *sql; unsigned i, count; if ((count = [_so count]) == 0) return nil; sql = [NSMutableString stringWithCapacity:(count * 16)]; for (i = 0; i < count; i++) { EOSortOrdering *so; NSString *column; SEL sel; so = [_so objectAtIndex:i]; sel = [so selector]; column = [self columnNameForFieldName:[so key]]; if (i > 0) [sql appendString:@", "]; if (sel_eq(sel, EOCompareAscending)) { [sql appendString:column]; [sql appendString:@" ASC"]; } else if (sel_eq(sel, EOCompareDescending)) { [sql appendString:column]; [sql appendString:@" DESC"]; } else if (sel_eq(sel, EOCompareCaseInsensitiveAscending)) { [sql appendString:@"UPPER("]; [sql appendString:column]; [sql appendString:@") ASC"]; } else if (sel_eq(sel, EOCompareCaseInsensitiveDescending)) { [sql appendString:@"UPPER("]; [sql appendString:column]; [sql appendString:@") DESC"]; } else { [self logWithFormat:@"cannot handle sort selector in store: %@", NSStringFromSelector(sel)]; } } return sql; } - (NSString *)generateSQLForQualifier:(EOQualifier *)_q { NSMutableString *ms; if (_q == nil) return nil; ms = [NSMutableString stringWithCapacity:32]; [_q _ocsAppendToString:ms]; return ms; } /* fetching */ - (NSArray *)fetchFields:(NSArray *)_flds fetchSpecification:(EOFetchSpecification *)_fs { EOQualifier *qualifier; NSArray *sortOrderings; EOAdaptorChannel *channel; NSException *error; NSMutableString *sql; NSArray *attrs; NSMutableArray *results; NSDictionary *row; qualifier = [_fs qualifier]; sortOrderings = [_fs sortOrderings]; #if 0 [self logWithFormat:@"FETCH: %@", _flds]; [self logWithFormat:@" MATCH: %@", _q]; #endif /* generate SQL */ sql = [NSMutableString stringWithCapacity:256]; [sql appendString:@"SELECT "]; if (_flds == nil) [sql appendString:@"*"]; else { unsigned i, count; count = [_flds count]; for (i = 0; i < count; i++) { if (i > 0) [sql appendString:@", "]; [sql appendString:[self columnNameForFieldName:[_flds objectAtIndex:i]]]; } } [sql appendString:@" FROM "]; [sql appendString:[self quickTableName]]; if (qualifier != nil) { [sql appendString:@" WHERE "]; [sql appendString:[self generateSQLForQualifier:qualifier]]; } if ([sortOrderings count] > 0) { [sql appendString:@" ORDER BY "]; [sql appendString:[self generateSQLForSortOrderings:sortOrderings]]; } #if 0 /* limit */ [sql appendString:@" LIMIT "]; // count [sql appendString:@" OFFSET "]; // index from 0 #endif /* open channel */ if ((channel = [self acquireStoreChannel]) == nil) { [self logWithFormat:@"ERROR(%s): could not open storage channel!"]; return nil; } /* run SQL */ if ((error = [channel evaluateExpressionX:sql]) != nil) { [self logWithFormat:@"ERROR(%s): cannot execute quick-fetch SQL '%@': %@", __PRETTY_FUNCTION__, sql, error]; [self releaseChannel:channel]; return nil; } /* fetch results */ results = [NSMutableArray arrayWithCapacity:64]; attrs = [channel describeResults]; while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) [results addObject:row]; /* release channels */ [self releaseChannel:channel]; return results; } - (NSArray *)fetchFields:(NSArray *)_flds matchingQualifier:(EOQualifier *)_q { EOFetchSpecification *fs; if (_q == nil) fs = nil; else { fs = [EOFetchSpecification fetchSpecificationWithEntityName: [self folderName] qualifier:_q sortOrderings:nil]; } return [self fetchFields:_flds fetchSpecification:fs]; } /* description */ - (NSString *)description { NSMutableString *ms; id tmp; ms = [NSMutableString stringWithCapacity:256]; [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])]; if (self->folderId) [ms appendFormat:@" id=%@", self->folderId]; else [ms appendString:@" no-id"]; if ((tmp = [self path])) [ms appendFormat:@" path=%@", tmp]; if ((tmp = [self folderTypeName])) [ms appendFormat:@" type=%@", tmp]; if ((tmp = [self location])) [ms appendFormat:@" loc=%@", [tmp absoluteString]]; [ms appendString:@">"]; return ms; } @end /* OCSFolder */