2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of OpenGroupware.org.
6 OGo is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 OGo is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with OGo; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "GCSFolder.h"
23 #include "GCSFolderManager.h"
24 #include "GCSFolderType.h"
25 #include "GCSChannelManager.h"
26 #include "GCSFieldExtractor.h"
27 #include "NSURL+GCS.h"
28 #include "EOAdaptorChannel+GCS.h"
29 #include "EOQualifier+GCS.h"
30 #include "GCSStringFormatter.h"
33 @implementation GCSFolder
35 static BOOL debugOn = NO;
36 static BOOL doLogStore = NO;
38 static Class NSStringClass = Nil;
39 static Class NSNumberClass = Nil;
40 static Class NSCalendarDateClass = Nil;
42 static GCSStringFormatter *stringFormatter = nil;
45 NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
47 debugOn = [ud boolForKey:@"GCSFolderDebugEnabled"];
48 doLogStore = [ud boolForKey:@"GCSFolderStoreDebugEnabled"];
50 NSStringClass = [NSString class];
51 NSNumberClass = [NSNumber class];
52 NSCalendarDateClass = [NSCalendarDate class];
54 stringFormatter = [GCSStringFormatter sharedFormatter];
57 - (id)initWithPath:(NSString *)_path primaryKey:(id)_folderId
58 folderTypeName:(NSString *)_ftname folderType:(GCSFolderType *)_ftype
59 location:(NSURL *)_loc quickLocation:(NSURL *)_qloc
60 folderManager:(GCSFolderManager *)_fm
62 if (![_loc isNotNull]) {
63 [self logWithFormat:@"ERROR(%s): missing quicktable parameter!",
69 if ((self = [super init])) {
70 self->folderManager = [_fm retain];
71 self->folderInfo = [_ftype retain];
73 self->folderId = [_folderId copy];
74 self->folderName = [[_path lastPathComponent] copy];
75 self->path = [_path copy];
76 self->location = [_loc retain];
77 self->quickLocation = _qloc ? [_qloc retain] : [_loc retain];
78 self->folderTypeName = [_ftname copy];
80 self->ofFlags.requiresFolderSelect = 0;
81 self->ofFlags.sameTableForQuick =
82 [self->location isEqualTo:self->quickLocation] ? 1 : 0;
87 return [self initWithPath:nil primaryKey:nil
88 folderTypeName:nil folderType:nil
89 location:nil quickLocation:nil
94 [self->folderManager release];
95 [self->folderInfo release];
96 [self->folderId release];
97 [self->folderName release];
99 [self->location release];
100 [self->quickLocation release];
101 [self->folderTypeName release];
107 - (NSNumber *)folderId {
108 return self->folderId;
111 - (NSString *)folderName {
112 return self->folderName;
118 - (NSURL *)location {
119 return self->location;
121 - (NSURL *)quickLocation {
122 return self->quickLocation;
125 - (NSString *)folderTypeName {
126 return self->folderTypeName;
129 - (GCSFolderManager *)folderManager {
130 return self->folderManager;
132 - (GCSChannelManager *)channelManager {
133 return [[self folderManager] channelManager];
136 - (NSString *)storeTableName {
137 return [[self location] gcsTableName];
139 - (NSString *)quickTableName {
140 return [[self quickLocation] gcsTableName];
143 - (BOOL)isQuickInfoStoredInContentTable {
144 return self->ofFlags.sameTableForQuick ? YES : NO;
149 - (EOAdaptorChannel *)acquireStoreChannel {
150 return [[self channelManager] acquireOpenChannelForURL:[self location]];
152 - (EOAdaptorChannel *)acquireQuickChannel {
153 return [[self channelManager] acquireOpenChannelForURL:[self quickLocation]];
156 - (void)releaseChannel:(EOAdaptorChannel *)_channel {
157 [[self channelManager] releaseChannel:_channel];
158 if (debugOn) [self debugWithFormat:@"released channel: %@", _channel];
161 - (BOOL)canConnectStore {
162 return [[self channelManager] canConnect:[self location]];
164 - (BOOL)canConnectQuick {
165 return [[self channelManager] canConnect:[self quickLocation]];
170 - (NSArray *)subFolderNames {
171 return [[self folderManager] listSubFoldersAtPath:[self path]
174 - (NSArray *)allSubFolderNames {
175 return [[self folderManager] listSubFoldersAtPath:[self path]
179 - (id)_fetchValueOfColumn:(NSString *)_col attributeName:(NSString *)_attrName
180 inContentWithName:(NSString *)_name
182 EOAdaptorChannel *channel;
189 if ((channel = [self acquireStoreChannel]) == nil) {
190 [self logWithFormat:@"ERROR(%s): could not open storage channel!",
191 __PRETTY_FUNCTION__];
198 sql = [sql stringByAppendingString:_col];
199 sql = [sql stringByAppendingString:@" FROM "];
200 sql = [sql stringByAppendingString:[self storeTableName]];
201 sql = [sql stringByAppendingString:@" WHERE \"c_name\" = '"];
202 sql = [sql stringByAppendingString:_name];
203 sql = [sql stringByAppendingString:@"'"];
207 if ((error = [channel evaluateExpressionX:sql]) != nil) {
208 [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@",
209 __PRETTY_FUNCTION__, sql, error];
210 [self releaseChannel:channel];
217 attrs = [channel describeResults];
218 if ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) {
219 result = [[[row objectForKey:_attrName] copy] autorelease];
220 if (![result isNotNull]) result = nil;
221 [channel cancelFetch];
224 /* release and return result */
226 [self releaseChannel:channel];
230 - (NSNumber *)versionOfContentWithName:(NSString *)_name {
231 return [self _fetchValueOfColumn:@"c_version" attributeName:@"cVersion"
232 inContentWithName:_name];
235 - (NSString *)fetchContentWithName:(NSString *)_name {
236 return [self _fetchValueOfColumn:@"c_content" attributeName:@"cContent"
237 inContentWithName:_name];
240 - (NSDictionary *)fetchContentsOfAllFiles {
242 Note: try to avoid the use of this method! The key of the dictionary
243 will be filename, the value the content.
245 NSMutableDictionary *result;
246 EOAdaptorChannel *channel;
252 if ((channel = [self acquireStoreChannel]) == nil) {
253 [self logWithFormat:@"ERROR(%s): could not open storage channel!",
254 __PRETTY_FUNCTION__];
260 sql = @"SELECT \"c_name\", \"c_content\" FROM ";
261 sql = [sql stringByAppendingString:[self storeTableName]];
265 if ((error = [channel evaluateExpressionX:sql]) != nil) {
266 [self logWithFormat:@"ERROR(%s): cannot execute SQL '%@': %@",
267 __PRETTY_FUNCTION__, sql, error];
268 [self releaseChannel:channel];
274 result = [NSMutableDictionary dictionaryWithCapacity:128];
275 attrs = [channel describeResults];
276 while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil) {
277 NSString *cName, *cContent;
279 cName = [row objectForKey:@"cName"];
280 cContent = [row objectForKey:@"cContent"];
282 if (![cName isNotNull]) {
283 [self logWithFormat:@"ERROR: missing cName in row: %@", row];
286 if (![cContent isNotNull]) {
287 [self logWithFormat:@"ERROR: missing cContent in row: %@", row];
291 [result setObject:cContent forKey:cName];
294 /* release and return result */
296 [self releaseChannel:channel];
300 /* writing content */
302 - (NSString *)_formatRowValue:(id)_value {
303 if (![_value isNotNull])
306 if ([_value isKindOfClass:NSStringClass])
307 return [stringFormatter stringByFormattingString:_value];
309 if ([_value isKindOfClass:NSNumberClass])
310 return [_value stringValue];
312 if ([_value isKindOfClass:NSCalendarDateClass]) {
313 /* be smart ... convert to timestamp */
314 return [NSString stringWithFormat:@"%i", [_value timeIntervalSince1970]];
317 [self logWithFormat:@"cannot handle value class: %@", [_value class]];
321 - (NSString *)_generateInsertStatementForRow:(NSDictionary *)_row
322 tableName:(NSString *)_table
324 // TODO: move to NSDictionary category?
325 NSMutableString *sql;
329 if (_row == nil || _table == nil)
332 keys = [_row allKeys];
334 sql = [NSMutableString stringWithCapacity:512];
335 [sql appendString:@"INSERT INTO "];
336 [sql appendString:_table];
337 [sql appendString:@" ("];
339 for (i = 0, count = [keys count]; i < count; i++) {
340 if (i != 0) [sql appendString:@", "];
341 [sql appendString:[keys objectAtIndex:i]];
344 [sql appendString:@") VALUES ("];
346 for (i = 0, count = [keys count]; i < count; i++) {
349 if (i != 0) [sql appendString:@", "];
350 value = [_row objectForKey:[keys objectAtIndex:i]];
351 value = [self _formatRowValue:value];
352 [sql appendString:value];
355 [sql appendString:@")"];
359 - (NSString *)_generateUpdateStatementForRow:(NSDictionary *)_row
360 tableName:(NSString *)_table
361 whereColumn:(NSString *)_colname isEqualTo:(id)_value
363 // TODO: move to NSDictionary category?
364 NSMutableString *sql;
368 if (_row == nil || _table == nil)
371 keys = [_row allKeys];
373 sql = [NSMutableString stringWithCapacity:512];
374 [sql appendString:@"UPDATE "];
375 [sql appendString:_table];
377 [sql appendString:@" SET "];
378 for (i = 0, count = [keys count]; i < count; i++) {
381 value = [_row objectForKey:[keys objectAtIndex:i]];
382 value = [self _formatRowValue:value];
384 if (i != 0) [sql appendString:@", "];
385 [sql appendString:[keys objectAtIndex:i]];
386 [sql appendString:@" = "];
387 [sql appendString:value];
390 [sql appendString:@" WHERE "];
391 [sql appendString:_colname];
392 [sql appendString:@" = "];
393 [sql appendString:[self _formatRowValue:_value]];
398 - (NSException *)writeContent:(NSString *)_content toName:(NSString *)_name {
399 EOAdaptorChannel *storeChannel, *quickChannel;
400 NSMutableDictionary *quickRow, *contentRow;
401 GCSFieldExtractor *extractor;
403 NSNumber *storedVersion;
405 NSCalendarDate *nowDate;
407 NSString *qsql, *bsql;
409 /* check preconditions */
412 return [NSException exceptionWithName:@"GCSStoreException"
413 reason:@"no content filename was provided"
416 if (_content == nil) {
417 return [NSException exceptionWithName:@"GCSStoreException"
418 reason:@"no content was provided"
425 nowDate = [NSCalendarDate date];
426 now = [NSNumber numberWithUnsignedInt:[nowDate timeIntervalSince1970]];
429 [self logWithFormat:@"should store content: '%@'\n%@", _name, _content];
431 storedVersion = [self versionOfContentWithName:_name];
433 [self logWithFormat:@" version: %@", storedVersion];
434 isNewRecord = [storedVersion isNotNull] ? NO : YES;
436 /* extract quick info */
438 extractor = [self->folderInfo quickExtractor];
439 quickRow = [extractor extractQuickFieldsFromContent:_content];
440 [quickRow setObject:_name forKey:@"c_name"];
443 [self logWithFormat:@" store quick: %@", quickRow];
445 /* make content row */
447 contentRow = [NSMutableDictionary dictionaryWithCapacity:16];
449 if (self->ofFlags.sameTableForQuick)
450 [contentRow addEntriesFromDictionary:quickRow];
452 [contentRow setObject:_name forKey:@"c_name"];
453 if (isNewRecord) [contentRow setObject:now forKey:@"c_creationdate"];
454 [contentRow setObject:now forKey:@"c_lastmodified"];
456 [contentRow setObject:[NSNumber numberWithInt:0] forKey:@"c_version"];
458 // TODO: increase version?
459 [contentRow setObject:[NSNumber numberWithInt:[storedVersion intValue]]
460 forKey:@"c_version"];
462 [contentRow setObject:_content forKey:@"c_content"];
466 if ((storeChannel = [self acquireStoreChannel]) == nil) {
467 [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
470 if (!self->ofFlags.sameTableForQuick) {
471 if ((quickChannel = [self acquireQuickChannel]) == nil) {
472 [self logWithFormat:@"ERROR(%s): could not open quick channel!"];
473 [self releaseChannel:storeChannel];
481 if (isNewRecord) { /* insert */
482 if (!self->ofFlags.sameTableForQuick) {
483 qsql = [self _generateInsertStatementForRow:quickRow
484 tableName:[self quickTableName]];
486 bsql = [self _generateInsertStatementForRow:contentRow
487 tableName:[self storeTableName]];
490 if (!self->ofFlags.sameTableForQuick) {
491 qsql = [self _generateUpdateStatementForRow:quickRow
492 tableName:[self quickTableName]
493 whereColumn:@"c_name" isEqualTo:_name];
495 bsql = [self _generateUpdateStatementForRow:contentRow
496 tableName:[self storeTableName]
497 whereColumn:@"c_name" isEqualTo:_name];
501 // TODO: execute in transactions
503 if ((error = [storeChannel evaluateExpressionX:bsql]) != nil) {
504 [self logWithFormat:@"ERROR(%s): cannot %s content '%@': %@",
505 __PRETTY_FUNCTION__, isNewRecord ? "insert" : "update", bsql, error];
508 if (error == nil && qsql != nil) {
509 if ((error = [quickChannel evaluateExpressionX:qsql]) != nil) {
513 [self logWithFormat:@"ERROR(%s): cannot %s quick '%@': %@",
514 __PRETTY_FUNCTION__, isNewRecord ? "insert" : "update",
518 /* insert in quick failed, so delete in content table */
520 delsql = [@"DELETE FROM " stringByAppendingString:
521 [self storeTableName]];
522 delsql = [delsql stringByAppendingString:@" WHERE c_name="];
523 delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
524 if ((delErr = [storeChannel evaluateExpressionX:delsql]) != nil) {
526 @"ERROR(%s): could not delete content '%@' after quick-fail:"
527 @" %@", __PRETTY_FUNCTION__, delsql, error];
533 [self releaseChannel:storeChannel];
534 if (!self->ofFlags.sameTableForQuick) [self releaseChannel:quickChannel];
538 - (NSException *)deleteContentWithName:(NSString *)_name {
539 EOAdaptorChannel *storeChannel, *quickChannel;
543 /* check preconditions */
546 return [NSException exceptionWithName:@"GCSDeleteException"
547 reason:@"no content filename was provided"
552 [self logWithFormat:@"should delete content: '%@'", _name];
556 if ((storeChannel = [self acquireStoreChannel]) == nil) {
557 [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
560 if (!self->ofFlags.sameTableForQuick) {
561 if ((quickChannel = [self acquireQuickChannel]) == nil) {
562 [self logWithFormat:@"ERROR(%s): could not open quick channel!"];
563 [self releaseChannel:storeChannel];
570 delsql = [@"DELETE FROM " stringByAppendingString:[self storeTableName]];
571 delsql = [delsql stringByAppendingString:@" WHERE c_name="];
572 delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
573 if ((error = [storeChannel evaluateExpressionX:delsql]) != nil) {
575 @"ERROR(%s): cannot delete content '%@': %@",
576 __PRETTY_FUNCTION__, delsql, error];
578 else if (!self->ofFlags.sameTableForQuick) {
579 /* content row deleted, now delete the quick row */
580 delsql = [@"DELETE FROM " stringByAppendingString:[self quickTableName]];
581 delsql = [delsql stringByAppendingString:@" WHERE c_name="];
582 delsql = [delsql stringByAppendingString:[self _formatRowValue:_name]];
583 if ((error = [quickChannel evaluateExpressionX:delsql]) != nil) {
585 @"ERROR(%s): cannot delete quick row '%@': %@",
586 __PRETTY_FUNCTION__, delsql, error];
588 Note: we now have a "broken" record, needs to be periodically GCed by
594 /* release channels and return */
596 [self releaseChannel:storeChannel];
597 if (!self->ofFlags.sameTableForQuick)
598 [self releaseChannel:quickChannel];
602 - (NSString *)columnNameForFieldName:(NSString *)_fieldName {
608 - (NSString *)generateSQLForSortOrderings:(NSArray *)_so {
609 NSMutableString *sql;
612 if ((count = [_so count]) == 0)
615 sql = [NSMutableString stringWithCapacity:(count * 16)];
616 for (i = 0; i < count; i++) {
621 so = [_so objectAtIndex:i];
623 column = [self columnNameForFieldName:[so key]];
625 if (i > 0) [sql appendString:@", "];
627 if (sel_eq(sel, EOCompareAscending)) {
628 [sql appendString:column];
629 [sql appendString:@" ASC"];
631 else if (sel_eq(sel, EOCompareDescending)) {
632 [sql appendString:column];
633 [sql appendString:@" DESC"];
635 else if (sel_eq(sel, EOCompareCaseInsensitiveAscending)) {
636 [sql appendString:@"UPPER("];
637 [sql appendString:column];
638 [sql appendString:@") ASC"];
640 else if (sel_eq(sel, EOCompareCaseInsensitiveDescending)) {
641 [sql appendString:@"UPPER("];
642 [sql appendString:column];
643 [sql appendString:@") DESC"];
646 [self logWithFormat:@"cannot handle sort selector in store: %@",
647 NSStringFromSelector(sel)];
653 - (NSString *)generateSQLForQualifier:(EOQualifier *)_q {
656 if (_q == nil) return nil;
657 ms = [NSMutableString stringWithCapacity:32];
658 [_q _gcsAppendToString:ms];
664 - (NSArray *)fetchFields:(NSArray *)_flds
665 fetchSpecification:(EOFetchSpecification *)_fs
667 EOQualifier *qualifier;
668 NSArray *sortOrderings;
669 EOAdaptorChannel *channel;
671 NSMutableString *sql;
673 NSMutableArray *results;
676 qualifier = [_fs qualifier];
677 sortOrderings = [_fs sortOrderings];
680 [self logWithFormat:@"FETCH: %@", _flds];
681 [self logWithFormat:@" MATCH: %@", _q];
686 sql = [NSMutableString stringWithCapacity:256];
687 [sql appendString:@"SELECT "];
689 [sql appendString:@"*"];
693 count = [_flds count];
694 for (i = 0; i < count; i++) {
695 if (i > 0) [sql appendString:@", "];
696 [sql appendString:[self columnNameForFieldName:[_flds objectAtIndex:i]]];
699 [sql appendString:@" FROM "];
700 [sql appendString:[self quickTableName]];
702 if (qualifier != nil) {
703 [sql appendString:@" WHERE "];
704 [sql appendString:[self generateSQLForQualifier:qualifier]];
706 if ([sortOrderings count] > 0) {
707 [sql appendString:@" ORDER BY "];
708 [sql appendString:[self generateSQLForSortOrderings:sortOrderings]];
712 [sql appendString:@" LIMIT "]; // count
713 [sql appendString:@" OFFSET "]; // index from 0
718 if ((channel = [self acquireStoreChannel]) == nil) {
719 [self logWithFormat:@"ERROR(%s): could not open storage channel!"];
725 if ((error = [channel evaluateExpressionX:sql]) != nil) {
726 [self logWithFormat:@"ERROR(%s): cannot execute quick-fetch SQL '%@': %@",
727 __PRETTY_FUNCTION__, sql, error];
728 [self releaseChannel:channel];
734 results = [NSMutableArray arrayWithCapacity:64];
735 attrs = [channel describeResults];
736 while ((row = [channel fetchAttributes:attrs withZone:NULL]) != nil)
737 [results addObject:row];
739 /* release channels */
741 [self releaseChannel:channel];
745 - (NSArray *)fetchFields:(NSArray *)_flds matchingQualifier:(EOQualifier *)_q {
746 EOFetchSpecification *fs;
751 fs = [EOFetchSpecification fetchSpecificationWithEntityName:
756 return [self fetchFields:_flds fetchSpecification:fs];
761 - (NSString *)description {
765 ms = [NSMutableString stringWithCapacity:256];
766 [ms appendFormat:@"<0x%08X[%@]:", self, NSStringFromClass([self class])];
769 [ms appendFormat:@" id=%@", self->folderId];
771 [ms appendString:@" no-id"];
773 if ((tmp = [self path])) [ms appendFormat:@" path=%@", tmp];
774 if ((tmp = [self folderTypeName])) [ms appendFormat:@" type=%@", tmp];
775 if ((tmp = [self location]))
776 [ms appendFormat:@" loc=%@", [tmp absoluteString]];
778 [ms appendString:@">"];