/***************************************************************** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. ****************************************************************/ #import "CAYObjectContext.h" #import "CAYQueryMessage.h" #import "CAYQuery.h" #import "CAYClientConnection.h" #import "CAYNodeDiff.h" #import "CAYNodePropertyChangeOperation.h" #import "CAYCompoundDiff.h" #import "CAYSyncMessage.h" #import "CAYObjEntity.h" #import "CAYObjRelationship.h" #import "CAYNodeCreateOperation.h" #import "CAYNodeDeleteOperation.h" #import "CAYCocoaCayenne.h" #import "CAYBootstrapMessage.h" #import "CAYArcCreateOperation.h" #import "CAYArcDeleteOperation.h" #import "CAYRelationshipQuery.h" #import "CAYGenericResponse.h" #import "CAYObjectIdQuery.h" #import "CAYUtil.h" @implementation CAYObjectContext -(id)init { self = [super init]; if(self != nil) { objectStore = [[CAYObjectStore alloc] init]; } return self; } -(NSArray *)performQuery:(CAYQuery *)query error:(NSError **)outError { CAYQueryMessage *queryMessage = [[CAYQueryMessage alloc] init]; [queryMessage setQuery:query]; id result = [[self connection] sendMessage:queryMessage]; [queryMessage release]; if([result isKindOfClass:[CAYGenericResponse class]]) { // ok } else if([result isKindOfClass:[NSError class]]) { // TODO: wrap or change code to make the NSError the most useful for the // framework user? NSLog(@"ERROR: NSError query result: %@", result); *outError = (NSError *)result; // http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles/cdFetching.html return nil; } else { NSLog(@"ERROR: unknown query result type %@: %@", [result class], result); NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"Unexpected server response: %@", nil, bundle, @""); NSString *errorMessage = [[[NSString alloc] initWithFormat:errorTmpl, result] autorelease]; NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorMessage forKey:NSLocalizedDescriptionKey]; NSError *error = [[[NSError alloc] initWithDomain:[[self class] description] code:0 userInfo:userInfoDict] autorelease]; *outError = error; // http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles/cdFetching.html return nil; } // a hack to let toOneFaults not refresh. without this hack // removing a element in a one-to-many relationship will not // be visible before a commit/query // TODO: clean up! perhaps support refreshing parameter on all queries? BOOL refreshing = YES; if([query isKindOfClass:[CAYRelationshipQuery class]]) { CAYRelationshipQuery *relq = (CAYRelationshipQuery *)query; refreshing = [relq refreshing]; } // TODO: results/rows are a mess. clean up! NSArray *results = [result results]; NSArray *rows = [results objectAtIndex:0]; // create a new result array as we may use some old objects instead of the // new ones. NSMutableArray *resultRows = [[NSMutableArray alloc] initWithCapacity:[rows count]]; // connect objects to the context for(id row in rows) { if([row isKindOfClass:[CAYPersistentObject class]]) { // try to look for old with same objectId in the context CAYPersistentObject *old = [[self objectStore] objectForId:[row objectId]]; if(old) { NSLog(@"DEBUG: old exist for oid. merge over values. %@ == %@", [row objectId], [old objectId]); // TODO: handle the state of the old object. merge over changes? or delete diffs // for objects refreshed from server? // clear out old values and set new values if(refreshing) { [[old valuesRaw] setDictionary:[row valuesRaw]]; [old setPersistenceState:PSTATE_COMMITTED]; } row = old; } else { NSLog(@"DEBUG: old does not exist for oid %@", [row objectId]); [[self objectStore] setObject:row forId:[row objectId]]; [row setPersistenceState:PSTATE_COMMITTED]; } // update resultRows as soon as possible [resultRows addObject:row]; [row setObjectContext:self]; // create faults for all relationships if(refreshing) { [row createFaults]; } } else { NSLog(@"ERROR: not able to handle returned row of type %@", [row class]); } } NSLog(@"DEBUG: query rows: %@", resultRows); return [resultRows autorelease]; } -(void) propertyChanged:(CAYPersistentObject *)object forProperty:(NSString *)property fromOld:(NSObject *)oldValue toNew:(NSObject *)newValue { NSLog(@"DEBUG: prop %@ changed from %@ to %@", property, oldValue, newValue); CAYNodeDiff *diff = [[CAYNodePropertyChangeOperation alloc] initWithNodeId:[object objectId] property:property oldValue:oldValue newValue:newValue]; [[self objectStore] registerDiff:diff]; [diff release]; [object setPersistenceState:PSTATE_MODIFIED]; NSLog(@"DEBUG: %i unsaved changes", [[self objectStore] diffCount]); } -(void) arcPropertyChanged:(CAYPersistentObject *)object forProperty:(NSString *)property fromOld:(NSObject *)oldValue toNew:(NSObject *)newValue { // NSLog(@"DEBUG: arc prop %@ changed from %@ to %@", property, oldValue, newValue); if([oldValue isKindOfClass:[CAYPersistentObject class]]) { NSLog(@"DEBUG: add a arcDelete %@.%@", [object class], property); CAYPersistentObject *ov = (CAYPersistentObject *)oldValue; CAYArcDeleteOperation *diff = [[CAYArcDeleteOperation alloc] initWithNodeId:[object objectId] targetNodeId:[ov objectId] arcId:property]; [[self objectStore] registerDiff:diff]; [diff release]; } if([newValue isKindOfClass:[CAYPersistentObject class]]) { NSLog(@"DEBUG: add a arcCreate %@.%@", [object class], property); CAYPersistentObject *nv = (CAYPersistentObject *)newValue; CAYArcCreateOperation *diff = [[CAYArcCreateOperation alloc] initWithNodeId:[object objectId] targetNodeId:[nv objectId] arcId:property]; [[self objectStore] registerDiff:diff]; [diff release]; } } -(void)setConnection:(CAYClientConnection *)c { [c retain]; [connection release]; connection = c; // TODO: move the rest of this method out of the setter? // send bootstrap message to get the EntityResolver CAYBootstrapMessage *bootstrapMsg = [[CAYBootstrapMessage alloc] init]; id bootresult = [[self connection] sendMessage:bootstrapMsg]; if([bootresult isKindOfClass:[CAYEntityResolver class]]) { NSLog(@"DEBUG: got an CAYEntityResolver: %@", bootresult); CAYEntityResolver *resolver = (CAYEntityResolver *)bootresult; [self setEntityResolver:resolver]; } else { NSLog(@"ERROR: bootstrap answer not of type CAYEntityResolver, but %@. %@", [bootresult class], bootresult); } [bootstrapMsg release]; // update class mapping based on EntityResolver [self updateClassMapping]; } -(CAYClientConnection *)connection { return connection; } -(CAYObjectStore *)objectStore { return objectStore; } -(BOOL)commitChanges:(NSError **)outError { NSLog(@"DEBUG: %i unsaved changes before commit.", [[self objectStore] diffCount]); CAYCompoundDiff *diffWithDiffs = [[CAYCompoundDiff alloc] initWithDiffs:[[self objectStore] diffs]]; CAYSyncMessage *msg = [[CAYSyncMessage alloc] initWithType:SYNCTYPE_FLUSH_CASCADE senderChanges:diffWithDiffs]; id result = [[self connection] sendMessage:msg]; NSLog(@"DEBUG: commit result: %@", result); // apply any resulting diffs. typicaly server generated primary key values // for new values if([result isKindOfClass:[CAYNodeDiff class]]) { CAYNodeDiff *resultDiff = (CAYNodeDiff *)result; [resultDiff apply:[self objectStore]]; // even if no diffs a CAYNodeDiff are returned. so this is the // place to clear out the diffs [[self objectStore] removeAllDiffs]; [[self undoManager] removeAllActions]; } else if ([result isKindOfClass:[NSError class]]) { // TODO: wrap or change code to make the NSError the most useful for the // framework user? NSLog(@"ERROR: NSError commit result"); *outError = (NSError *)result; return NO; } else { NSLog(@"ERROR: unknown commit result type %@", [result class]); NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"Unexpected server response: %@", nil, bundle, @""); NSString *errorMessage = [[[NSString alloc] initWithFormat:errorTmpl, result] autorelease]; NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorMessage forKey:NSLocalizedDescriptionKey]; NSError *error = [[[NSError alloc] initWithDomain:[[self class] description] code:0 userInfo:userInfoDict] autorelease]; *outError = error; return NO; } [msg release]; [diffWithDiffs release]; NSLog(@"DEBUG: %i unsaved changes after commit", [[self objectStore] diffCount]); return YES; } -(void)setEntityResolver:(CAYEntityResolver *)er { [er retain]; [entityResolver release]; entityResolver = er; } -(CAYEntityResolver *)entityResolver { return entityResolver; } -(CAYPersistentObject *)newObject:(Class)pc { CAYPersistentObject *o = [[pc alloc] init]; [self registerNewObject:o]; return [o autorelease]; } -(void)registerNewObject:(CAYPersistentObject *)o { NSLog(@"DEBUG: register new object"); // TODO: check existing data context? [o retain]; [o setObjectContext:self]; // set persistant state [o setPersistenceState:PSTATE_NEW]; CAYObjectId *oid = [[CAYObjectId alloc] init]; // need to search by class as o does not have an ObjectId with entityName yet CAYObjEntity *objEntity = [[self entityResolver] objEntityForClass:[o class]]; [oid setEntityName:[objEntity name]]; [oid setTempKey:[CAYUtil createRandomDataWithLength:8]]; [o setObjectId:oid]; CAYNodeDiff *diff = [[CAYNodeCreateOperation alloc] initWithNodeId:oid]; [[self objectStore] registerDiff:diff]; [diff release]; [[self objectStore] setObject:o forId:oid]; [oid release]; [o awakeFromInsert]; [o release]; } -(void)deleteObject:(CAYPersistentObject *)o { CAYNodeDiff *diff = [[CAYNodeDeleteOperation alloc] initWithNodeId:[o objectId]]; [[self objectStore] registerDiff:diff]; [diff release]; // remove from object store [[self objectStore] removeObjectForId:[o objectId]]; // set persistent state [o setPersistenceState:PSTATE_DELETED]; // cleanup and remove [o setObjectContext:nil]; [o setObjectId:nil]; } -(void)prepareForAccess:(CAYPersistentObject *)object forProperty:(NSString *)property withLazyFetching:(BOOL)lazyFaulting { if([object persistenceState] == PSTATE_HOLLOW) { NSLog(@"DEBUG: Object id hollow - issuing a ObjectIdQuery. %@", [object objectId]); CAYObjectId *oid = [object objectId]; CAYObjectIdQuery *query = [[CAYObjectIdQuery alloc] init]; [query setObjectId:oid]; NSError *error = nil; NSArray *rows = [self performQuery:query error:&error]; [query release]; if(!rows) { // TODO: do something with this error?? NSLog(@"ERROR: %@", [error localizedDescription]); } if([rows count] == 0) { NSLog(@"ERROR: Error resolving fault, no matching row exists in the database for ObjectId: %@", oid); } if([rows count] > 1) { NSLog(@"ERROR: Error resolving fault, more than one row exists in the database for ObjectId: %@", oid); } if([object persistenceState] != PSTATE_COMMITTED) { NSLog(@"ERROR: Error resolving fault for ObjectId: %@. Persistence state not comitted after ObjectIdQuery. %d", oid, [object persistenceState]); } } // TODO: resolve faults? } -(void)setUndoManager:(NSUndoManager *)um { [um retain]; [undoManager release]; undoManager = um; } -(NSUndoManager *)undoManager { return undoManager; } -(void)updateClassMapping { if([self entityResolver]) { // full class mapping for cayenne classes and all entities NSLog(@"DEBUG: update class mapping from entity resolver"); [[self entityResolver] updateClassMapping]; [[self connection] updateClassMapping:[[self entityResolver] classMapping]]; } else { // basic class mapping for the cayenne classes NSLog(@"DEBUG: update cayenne class"); [[self connection] updateClassMapping:[CAYCocoaCayenne classMapping]]; } } -(void)dealloc { [self setConnection:nil]; [self setEntityResolver:nil]; [objectStore release]; objectStore = nil; [self setUndoManager:nil]; [super dealloc]; } @end