/* SOGoCacheGCSObject.m - this file is part of SOGo * * Copyright (C) 2012-2014 Inverse inc * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3, or (at your option) * any later version. * * This file 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "GCSSpecialQueries+SOGoCacheObject.h" #import "SOGoCacheGCSFolder.h" #import "BSONCodec.h" #import "SOGoCacheGCSObject.h" static EOAttribute *textColumn = nil; @implementation SOGoCacheGCSObject + (void) initialize { NSDictionary *description; if (!textColumn) { /* TODO: this is a hack for providing an EOAttribute definition that is compatible with all the backends that we support. We should make use of EOModel instead. */ description = [NSDictionary dictionaryWithObjectsAndKeys: @"c_textfield", @"columnName", @"VARCHAR", @"externalType", nil]; textColumn = [EOAttribute attributeFromPropertyList: description]; [textColumn retain]; } } /* = (@"CREATE TABLE %@ (" @" c_path VARCHAR(255) PRIMARY KEY," @" c_parent_path VARCHAR(255)," @" c_type SMALLINT NOT NULL," @" c_creationdate INT4 NOT NULL," @" c_lastmodified INT4 NOT NULL," @" c_version INT4 NOT NULL DEFAULT 0," @" c_deleted SMALLINT NOT NULL DEFAULT 0," @" c_content TEXT)"); */ - (id) init { if ((self = [super init])) { tableUrl = nil; initialized = NO; objectType = -1; deleted = NO; version = 0; } return self; } - (void) dealloc { [tableUrl release]; [super dealloc]; } - (void) setTableUrl: (NSURL *) newTableUrl { ASSIGN (tableUrl, newTableUrl); } - (NSURL *) tableUrl { if (!tableUrl) { tableUrl = [container tableUrl]; [tableUrl retain]; if (!tableUrl) [NSException raise: @"SOGoCacheIOException" format: @"table url is not set for object '%@'", self]; } return tableUrl; } - (NSString *) tableName { NSArray *parts; [self tableUrl]; parts = [[tableUrl path] componentsSeparatedByString: @"/"]; return [parts lastObject]; } - (void) setupFromRecord: (NSDictionary *) record { NSInteger intValue; NSString *propsValue; NSDictionary *newValues; objectType = [[record objectForKey: @"c_type"] intValue]; intValue = [[record objectForKey: @"c_creationdate"] intValue]; ASSIGN (creationDate, [NSCalendarDate dateWithTimeIntervalSince1970: (NSTimeInterval) intValue]); intValue = [[record objectForKey: @"c_lastmodified"] intValue]; ASSIGN (lastModified, [NSCalendarDate dateWithTimeIntervalSince1970: (NSTimeInterval) intValue]); deleted = ([[record objectForKey: @"c_deleted"] intValue] > 0); version = [[record objectForKey: @"c_version"] intValue]; propsValue = [record objectForKey: @"c_content"]; if ([propsValue isNotNull]) { newValues = [[propsValue dataByDecodingBase64] BSONValue]; [properties addEntriesFromDictionary: newValues]; } else [properties removeAllObjects]; initialized = YES; } /* accessors */ - (NSMutableString *) path { NSMutableString *path; if (container) path = [container pathForChild: nameInContainer]; else path = [NSMutableString stringWithFormat: @"/%@", nameInContainer]; if ([path rangeOfString: @"//"].location != NSNotFound) [NSException raise: @"SOGoCacheIOException" format: @"object path has not been properly set for" " folder '%@' (%@)", self, path]; return path; } - (void) setObjectType: (SOGoCacheObjectType) newObjectType { objectType = newObjectType; } - (SOGoCacheObjectType) objectType /* message, fai, folder */ { return objectType; } - (NSCalendarDate *) creationDate { if (!initialized) [NSException raise: @"SOGoCacheIOException" format: @"record has not been initialized: %@", self]; return creationDate; } - (NSCalendarDate *) lastModified { if (!initialized) [NSException raise: @"SOGoCacheIOException" format: @"record has not been initialized: %@", self]; return lastModified; } - (BOOL) deleted { return deleted; } /* actions */ - (void) setNameInContainer: (NSString *) newNameInContainer { NSMutableString *sql; NSString *oldPath, *newPath; if (nameInContainer) oldPath = [self path]; [super setNameInContainer: newNameInContainer]; if (nameInContainer) { newPath = [self path]; sql = [NSMutableString stringWithFormat: @"UPDATE %@" @" SET c_path = '%@'", [self tableName], newPath]; [sql appendFormat: @" WHERE c_path = '%@'", oldPath]; [self performBatchSQLQueries: [NSArray arrayWithObject: sql]]; } } - (void) changePathTo: (NSString *) newPath { NSMutableString *sql; NSString *oldPath, *newParentPath; NSRange slashRange; oldPath = [self path]; slashRange = [newPath rangeOfString: @"/" options: NSBackwardsSearch]; if (slashRange.location != NSNotFound) newParentPath = [newPath substringToIndex: slashRange.location]; else newParentPath = NULL; sql = [NSMutableString stringWithFormat: @"UPDATE %@" @" SET c_path = '%@'", [self tableName], newPath]; if (newParentPath) [sql appendFormat: @", c_parent_path = '%@'", newParentPath]; else [sql appendString: @", c_parent_path = NULL"]; [sql appendFormat: @" WHERE c_path = '%@'", oldPath]; [self performBatchSQLQueries: [NSArray arrayWithObject: sql]]; } - (EOAdaptor *) tableChannelAdaptor { GCSChannelManager *cm; EOAdaptor *adaptor; EOAdaptorChannel *channel; cm = [GCSChannelManager defaultChannelManager]; channel = [cm acquireOpenChannelForURL: [self tableUrl]]; adaptor = [[channel adaptorContext] adaptor]; [cm releaseChannel: channel]; return adaptor; } - (NSArray *) performSQLQuery: (NSString *) sql { NSMutableArray *records; GCSChannelManager *cm; EOAdaptorChannel *channel; NSException *error; NSArray *attrs; NSDictionary *record; cm = [GCSChannelManager defaultChannelManager]; channel = [cm acquireOpenChannelForURL: [self tableUrl]]; error = [channel evaluateExpressionX: sql]; if (error) { records = nil; [self logWithFormat: @"an exception occurred when executing query '%@'", sql]; [self logWithFormat: @"exception is '%@'", error]; } else { records = [NSMutableArray arrayWithCapacity: 256]; attrs = [channel describeResults: NO]; while ((record = [channel fetchAttributes: attrs withZone: NULL])) [records addObject: record]; } [cm releaseChannel: channel]; return records; } - (BOOL) performBatchSQLQueries: (NSArray *) queries { GCSChannelManager *cm; EOAdaptorChannel *channel; EOAdaptorContext *dbContext; NSException *error; NSUInteger count, max; NSString *sql; cm = [GCSChannelManager defaultChannelManager]; channel = [cm acquireOpenChannelForURL: [self tableUrl]]; dbContext = [channel adaptorContext]; [dbContext beginTransaction]; error = nil; max = [queries count]; for (count = 0; error == nil && count < max; count++) { sql = [queries objectAtIndex: count]; error = [channel evaluateExpressionX: sql]; if (error) [dbContext rollbackTransaction]; } if (!error) [dbContext commitTransaction]; [cm releaseChannel: channel]; return (error == nil); } - (NSDictionary *) lookupRecord: (NSString *) path newerThanVersion: (NSInteger) startVersion { NSDictionary *record; NSArray *records; NSString *tableName, *pathValue; NSMutableString *sql; EOAdaptor *adaptor; if ([path hasSuffix: @"/"]) [NSException raise: @"SOGoCacheIOException" format: @"path ends with a slash: %@", path]; tableName = [self tableName]; adaptor = [self tableChannelAdaptor]; pathValue = [adaptor formatValue: path forAttribute: textColumn]; /* query */ sql = [NSMutableString stringWithFormat: @"SELECT * FROM %@ WHERE c_path = %@", tableName, pathValue]; if (startVersion > -1) [sql appendFormat: @" AND c_version > %d", startVersion]; /* execution */ records = [self performSQLQuery: sql]; if ([records count] > 0) record = [records objectAtIndex: 0]; else record = nil; return record; } // get a list of all folders - (NSArray *) folderList: (NSString *) deviceId newerThanVersion: (NSInteger) startVersion { NSMutableArray *recordsOut; NSArray *records; NSString *tableName, *pathValue; NSMutableString *sql; EOAdaptor *adaptor; NSUInteger count, max; if ([deviceId hasSuffix: @"/"]) [NSException raise: @"SOGoCacheIOException" format: @"path ends with a slash: %@", deviceId]; tableName = [self tableName]; adaptor = [self tableChannelAdaptor]; pathValue = [adaptor formatValue: [NSString stringWithFormat: @"/%@+folder%", deviceId] forAttribute: textColumn]; /* query */ sql = [NSMutableString stringWithFormat: @"SELECT * FROM %@ WHERE c_path LIKE %@ AND c_deleted <> 1", tableName, pathValue]; if (startVersion > -1) [sql appendFormat: @" AND c_version > %d", startVersion]; /* execution */ records = [self performSQLQuery: sql]; max = [records count]; recordsOut = [[NSMutableArray alloc] init]; for (count = 0; count < max; count++) { [recordsOut addObject: [[records objectAtIndex: count] objectForKey: @"c_path"]]; } return recordsOut; } - (void) reloadIfNeeded { /* if object is uninitialized: reload without condition, otherwise, load if c_version > :version */ NSDictionary *record; if (initialized) { if (!isNew) { record = [self lookupRecord: [self path] newerThanVersion: version]; if (record) [self setupFromRecord: record]; } } else { record = [self lookupRecord: [self path] newerThanVersion: -1]; if (record) { [self setupFromRecord: record]; isNew = NO; } else isNew = YES; initialized = YES; } } - (NSException *) delete { deleted = YES; [properties removeAllObjects]; [self save]; return nil; } - (NSException *) destroy { NSString *tableName, *pathValue, *sql; EOAdaptorChannel *channel; GCSChannelManager *cm; NSException *result; EOAdaptor *adaptor; cm = [GCSChannelManager defaultChannelManager]; channel = [cm acquireOpenChannelForURL: [self tableUrl]]; tableName = [self tableName]; adaptor = [[channel adaptorContext] adaptor]; pathValue = [adaptor formatValue: [self path] forAttribute: textColumn]; result = nil; sql = [NSString stringWithFormat: (@"DELETE FROM %@" @" WHERE c_path = %@"), tableName, pathValue]; result = [channel evaluateExpressionX: sql]; if (result) [self errorWithFormat: @"could not delete record %@" @" in %@: %@", pathValue, tableName, result]; [cm releaseChannel: channel]; return result; } - (void) save { NSString *sql; NSData *content; NSCalendarDate *now; GCSChannelManager *cm; EOAdaptor *adaptor; EOAdaptorChannel *channel; NSInteger creationDateValue, lastModifiedValue, deletedValue; NSString *tableName, *pathValue, *parentPathValue, *propsValue; NSException *result; if (!initialized) [NSException raise: @"SOGoCacheIOException" format: @"record has not been initialized: %@", self]; cm = [GCSChannelManager defaultChannelManager]; channel = [cm acquireOpenChannelForURL: [self tableUrl]]; tableName = [self tableName]; now = [NSCalendarDate date]; ASSIGN (lastModified, now); adaptor = [[channel adaptorContext] adaptor]; pathValue = [adaptor formatValue: [self path] forAttribute: textColumn]; lastModifiedValue = (NSInteger) [lastModified timeIntervalSince1970]; if (objectType == -1) [NSException raise: @"SOGoCacheIOException" format: @"object type has not been set for object '%@'", self]; if ([properties count] > 0) { content = [properties BSONRepresentation]; propsValue = [adaptor formatValue: [content stringByEncodingBase64] forAttribute: textColumn]; } else propsValue = @"NULL"; if (isNew) { ASSIGN (creationDate, now); creationDateValue = (NSInteger) [creationDate timeIntervalSince1970]; parentPathValue = [adaptor formatValue: [container path] forAttribute: textColumn]; if (!parentPathValue) parentPathValue = @"NULL"; sql = [NSString stringWithFormat: (@"INSERT INTO %@" @" (c_path, c_parent_path, c_type, c_creationdate, c_lastmodified," @" c_deleted, c_version, c_content)" @" VALUES (%@, %@, %d, %d, %d, 0, 0, %@" @")"), tableName, pathValue, parentPathValue, objectType, creationDateValue, lastModifiedValue, propsValue]; isNew = NO; } else { version++; deletedValue = (deleted ? 1 : 0); sql = [NSString stringWithFormat: (@"UPDATE %@" @" SET c_lastmodified = %d, c_deleted = %d," @" c_version = %d, c_content = %@" @" WHERE c_path = %@"), tableName, lastModifiedValue, deletedValue, version, propsValue, pathValue]; } result = [channel evaluateExpressionX: sql]; if (result) [self errorWithFormat: @"could not insert/update record for record %@" @" in %@: %@", pathValue, tableName, result]; [cm releaseChannel: channel]; } @end