/***************************************************************** * 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 "CAYPersistentObject.h" #import "CAYObjectId.h" #import "CAYObjectContext.h" #import "CAYFault.h" #import "CAYObjEntity.h" #import "CAYObjRelationship.h" #import "CAYCocoaCayenne.h" #import "CAYFault.h" #import "CAYToManyFault.h" #import "CAYToOneFault.h" #import "CAYObjAttribute.h" @interface CAYPersistentObject (PrivateMethods) -(void)createValidationError:(NSString *)errorMessage code:(NSInteger)errorCode error:(NSError **)outError; -(NSUndoManager *)undoManager; @end @implementation CAYPersistentObject -(id)init { self = [super init]; if(self) { values = [[NSMutableDictionary dictionary] retain]; [self setPersistenceState:PSTATE_TRANSIENT]; } return self; } -(id)initWithCoder:(NSCoder*)coder { [self init]; [self setObjectId:[coder decodeObjectForKey:@"objectId"]]; return self; } -(void)encodeWithCoder:(NSCoder*)coder { [coder encodeObject:objectId forKey:@"objectId"]; } -(void)setObjectId:(CAYObjectId *)oid { [oid retain]; [objectId release]; objectId = oid; } -(CAYObjectId *)objectId { return objectId; } -(void)setPersistenceState:(NSUInteger)state { persistenceState = state; if(persistenceState == PSTATE_HOLLOW) { NSLog(@"DEBUG: setting persistent state hollow - clearing values. %@", [self objectId]); [[self valuesRaw] removeAllObjects]; } } -(NSUInteger)persistenceState { return persistenceState; } -(void)setObjectContext:(CAYObjectContext *)ctxt { // do not need to retain our master objectContext = ctxt; } -(CAYObjectContext *)objectContext { return objectContext; } -(NSString *)description { NSMutableDictionary *descvalues = [[NSMutableDictionary alloc] init]; NSEnumerator *enumerator = [values keyEnumerator]; NSString *attrname; while(attrname = [enumerator nextObject]) { id value = [values objectForKey:attrname]; if([value isKindOfClass:[CAYPersistentObject class]]) { [descvalues setObject:[value objectId] forKey:attrname]; } else if ([value isKindOfClass:[NSArray class]]) { [descvalues setObject:@"(..)" forKey:attrname]; } else if ([value isKindOfClass:[CAYFault class]]) { [descvalues setObject:@"?" forKey:attrname]; } else { [descvalues setObject:value forKey:attrname]; } } NSString *result; result = [[NSString alloc] initWithFormat:@"%@ {objectId = %@; values = %@}", [self class], [self objectId], descvalues]; [descvalues release]; [result autorelease]; return result; } -(NSMutableDictionary *)valuesRaw { return values; } -(void)createFaults { CAYObjEntity *objEntity = [[[self objectContext] entityResolver] lookupObjEntity:self]; NSDictionary *relations = [objEntity relationships]; NSEnumerator *enumerator = [relations keyEnumerator]; NSString *relname; while(relname = [enumerator nextObject]) { CAYObjRelationship *rel = (CAYObjRelationship *)[relations objectForKey:relname]; NSLog(@"DEBUG: create fault for relation %@.%@: %@", [objEntity name], relname, rel); CAYFault *fault = nil; if([rel isToMany]) { fault = [[CAYToManyFault alloc] initWithSourceObject:self relationshipName:relname]; } else { fault = [[CAYToOneFault alloc] initWithSourceObject:self relationshipName:relname]; } [[self valuesRaw] setValue:fault forKey:relname]; [fault release]; } } -(void)setPrimitiveValue:(id)value forKey:(NSString *)key; { // NSLog(@"DEBUG: set value %@ for key %@ of type %@", value, key, [value class]); [self willChangeValueForKey:key]; // see if the key is a relationship CAYObjEntity *objEntity = [[[self objectContext] entityResolver] lookupObjEntity:self]; CAYObjRelationship *relationship = [[objEntity relationships] valueForKey:key]; id oldValue = [self valueForKey:key]; if (relationship) { if ([relationship isToMany]) { // check that arg are NSArray if(![value isKindOfClass:[NSArray class]]) { NSLog(@"ERROR: toMany argument should be of type NSArray, not %@", [value class]); } //NSArray *oldValue = [self valueForKey:key]; CAYPersistentObject *element; // handle new objects for the relationship NSMutableArray *newElements = [[NSMutableArray alloc] init]; [newElements addObjectsFromArray:value]; [newElements removeObjectsInArray:oldValue]; for(element in newElements) { NSLog(@"DEBUG: new element for rel %@: %@", key, element); [self addToManyTarget:element forKey:key setReverse:YES]; } [newElements release]; // handle objects that are not longer in the relationship NSMutableArray *removedElements = [[NSMutableArray alloc] init]; [removedElements addObjectsFromArray:oldValue]; [removedElements removeObjectsInArray:value]; for(element in removedElements) { NSLog(@"DEBUG: remove element for rel %@: %@", key, element); [self removeToManyTarget:element forKey:key setReverse:YES]; } [removedElements release]; } else { // check that arg are CAYPersistentObject if(![value isKindOfClass:[CAYPersistentObject class]]) { NSLog(@"ERROR: toMany argument should be of type CAYPersistentObject, not %@", [value class]); } [self setToOneTarget:value forKey:key setReverse:YES]; } } else { // a none-relationship property //id oldValue = [self valueForKey:key]; // create diff and set value. validation done using key/value validation [[self objectContext] propertyChanged:self forProperty:key fromOld:[values objectForKey:key] toNew:value]; [values setValue:value forKey:key]; CAYObjAttribute *attribute = [[objEntity attributes] valueForKey:key]; NSLog(@"DEBUG: %@.%@. attribute: %@. oldValue: %@. oldValue class: %@",[objEntity name], key, attribute, oldValue, [oldValue class]); } [[[self undoManager] prepareWithInvocationTarget:self] setPrimitiveValue:oldValue forKey:key]; [self didChangeValueForKey:key]; } // this method is called if setValue:forKey: was not able to find a user defined set method for the key // see http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueCoding/Concepts/SearchImplementation.html -(void)setValue:(id)value forUndefinedKey:(NSString *)key { [[self objectContext] prepareForAccess:self forProperty:key withLazyFetching:NO]; [self setPrimitiveValue:value forKey:key]; } -(id)primitiveValueForKey:(NSString *)key { id val = [values objectForKey:key]; if([val isKindOfClass:[CAYFault class]]) { CAYFault *fault = (CAYFault *)val; NSLog(@"DEBUG: resolve fault %@", fault); val = [fault resolveFault]; [values setValue:val forKey:key]; } if([val isKindOfClass:[NSNull class]]) { // return nil instead of NSNull as it works better for formatters++ return nil; } return val; } // this method is called if valueForKey: was not able to find a user defined get method for the key // see http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueCoding/Concepts/SearchImplementation.html -(id)valueForUndefinedKey:(NSString *)key { [[self objectContext] prepareForAccess:self forProperty:key withLazyFetching:NO]; return [self primitiveValueForKey:key]; } -(BOOL)validateValue:(id *)ioValue forKey:(NSString *)key error:(NSError **)outError { CAYObjEntity *objEntity = [[[self objectContext] entityResolver] lookupObjEntity:self]; CAYObjAttribute *attribute = [[objEntity attributes] valueForKey:key]; // http://developer.apple.com/documentation/Cocoa/Conceptual/CoreData/Articles/cdValidation.html // check for mandatory field if([attribute isMandatory]) { if((!*ioValue) || [*ioValue isKindOfClass:[NSNull class]]) { NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"\"%@\" is mandatory", nil, bundle, @""); NSString *errorMsg = [[NSString alloc] initWithFormat:errorTmpl, [attribute name]]; [self createValidationError:errorMsg code:VALIDATION_MANDATORY error:outError]; [errorMsg release]; return NO; } } if([[attribute javaType] isEqualToString:@"java.lang.String"]) { if([*ioValue isKindOfClass:[NSString class]]) { if(([attribute maxLength] > 0) && ([*ioValue length] > [attribute maxLength])) { NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"\"%@\" exceeds maximum allowed length (%d chars): %d", nil, bundle, @""); NSString *errorMsg = [[NSString alloc] initWithFormat:errorTmpl, [attribute name], [attribute maxLength], [*ioValue length]]; [self createValidationError:errorMsg code:VALIDATION_LENGTH error:outError]; [errorMsg release]; return NO; } return YES; } NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"\"%@\" must be a NSString, not a %@", nil, bundle, @""); NSString *errorMsg = [[NSString alloc] initWithFormat:errorTmpl, [attribute name], [*ioValue class]]; [self createValidationError:errorMsg code:VALIDATION_TYPE error:outError]; [errorMsg release]; return NO; } else if([[attribute javaType] isEqualToString:@"java.util.Date"]) { if([*ioValue isKindOfClass:[NSDate class]]) { return YES; } NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"\"%@\" must be a NSDate, not a %@", nil, bundle, @""); NSString *errorMsg = [[NSString alloc] initWithFormat:errorTmpl, [attribute name], [*ioValue class]]; [self createValidationError:errorMsg code:VALIDATION_TYPE error:outError]; [errorMsg release]; return NO; } else if([[attribute javaType] isEqualToString:@"java.lang.Integer"]) { if([*ioValue isKindOfClass:[NSNumber class]]) { if(strcmp("i",[*ioValue objCType]) != 0) { // try to convert non-integer numbers to int *ioValue = [NSNumber numberWithInteger:[*ioValue integerValue]]; } return YES; } NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"\"%@\" must be a NSNumber, not a %@", nil, bundle, @""); NSString *errorMsg = [[NSString alloc] initWithFormat:errorTmpl, [attribute name], [*ioValue class]]; [self createValidationError:errorMsg code:VALIDATION_TYPE error:outError]; [errorMsg release]; return NO; } else if([[attribute javaType] isEqualToString:@"java.lang.Float"]) { if([*ioValue isKindOfClass:[NSNumber class]]) { if(strcmp("f",[*ioValue objCType]) != 0) { // try to convert non-float numbers to float *ioValue = [NSNumber numberWithFloat:[*ioValue floatValue]]; } return YES; } NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"\"%@\" must be a NSNumber, not a %@", nil, bundle, @""); NSString *errorMsg = [[NSString alloc] initWithFormat:errorTmpl, [attribute name], [*ioValue class]]; [self createValidationError:errorMsg code:VALIDATION_TYPE error:outError]; [errorMsg release]; return NO; } else if([[attribute javaType] isEqualToString:@"java.lang.Double"]) { if([*ioValue isKindOfClass:[NSNumber class]]) { if(strcmp("d",[*ioValue objCType]) != 0) { // try to convert non-float numbers to float *ioValue = [NSNumber numberWithDouble:[*ioValue doubleValue]]; } return YES; } NSBundle *bundle = [CAYCocoaCayenne bundle]; NSString *errorTmpl = NSLocalizedStringFromTableInBundle(@"\"%@\" must be a NSNumber, not a %@", nil, bundle, @""); NSString *errorMsg = [[NSString alloc] initWithFormat:errorTmpl, [attribute name], [*ioValue class]]; [self createValidationError:errorMsg code:VALIDATION_TYPE error:outError]; [errorMsg release]; return NO; } else { NSLog(@"ERROR: unhandled java type %@ - ignoring any validation errors", attribute); return YES; } } -(void)setToOneTarget:(CAYPersistentObject *)value forKey:(NSString *)key setReverse:(BOOL)setrev { // NSLog(@"DEBUG: setToOneTarget value class: %@, key: %@, setrev: %@", [value class], key, setrev ? @"YES" : @"NO"); [self willConnect:value forKey:key]; id oldTarget = [self valueForKey:key]; // check for special case of no change if(oldTarget == value) { return; } [[self objectContext] arcPropertyChanged:self forProperty:key fromOld:oldTarget toNew:value]; if(setrev) { if([oldTarget isKindOfClass:[CAYPersistentObject class]]) { [self unsetReverseRelationship:oldTarget forKey:key]; } if(value) { [self setReverseRelationship:value forKey:key]; } } [values setValue:value forKey:key]; } -(void)addToManyTarget:(CAYPersistentObject *)value forKey:(NSString *)key setReverse:(BOOL)setrev { // NSLog(@"DEBUG: addToManyTarget value class: %@, key: %@, setrev: %@", [value class], key, setrev ? @"YES" : @"NO"); [self willConnect:value forKey:key]; if(!value) { // TODO: throw. NSLog(@"ERROR. can not add nil to %@.%@", [self class], key); return; } NSMutableArray *list = [self valueForKey:key]; [[self objectContext] arcPropertyChanged:self forProperty:key fromOld:nil toNew:value]; [list addObject:value]; if(setrev) { [self setReverseRelationship:value forKey:key]; } } -(void)removeToManyTarget:(CAYPersistentObject *)value forKey:(NSString *)key setReverse:(BOOL)setrev { // NSLog(@"DEBUG: removeToManyTarget value class: %@, key: %@, setrev: %@", [value class], key, setrev ? @"YES" : @"NO"); if(!value) { NSLog(@"ERROR. can not remove nil from %@.%@. ignoring.", [self class], key); return; } NSMutableArray *list = [self valueForKey:key]; [[self objectContext] arcPropertyChanged:self forProperty:key fromOld:value toNew:nil]; if([self persistenceState] == PSTATE_COMMITTED) { [self setPersistenceState:PSTATE_MODIFIED]; } // NSLog(@"DEBUG: value for %@.%@ is of type %@", [self class], key, [list class]); [list removeObject:value]; if(setrev) { [self unsetReverseRelationship:value forKey:key]; } } -(void)setReverseRelationship:(CAYPersistentObject *)value forKey:(NSString *)key { // NSLog(@"DEBUG: setReverseRelationship value class: %@, key: %@", [value class], key); // find relationship CAYObjEntity *objEntity = [[[self objectContext] entityResolver] lookupObjEntity:self]; CAYObjRelationship *relationship = [[objEntity relationships] valueForKey:key]; // find reverse relationship. could be nil. CAYObjEntity *revObjEntity = [[[self objectContext] entityResolver] lookupObjEntity:value]; CAYObjRelationship *revRelationship = [[revObjEntity relationships] valueForKey:[relationship reverseRelationshipName]]; if(revRelationship) { if([revRelationship isToMany]) { [value addToManyTarget:self forKey:[revRelationship name] setReverse:NO]; } else { [value setToOneTarget:self forKey:[revRelationship name] setReverse:NO]; } } } -(void)unsetReverseRelationship:(CAYPersistentObject *)value forKey:(NSString *)key { // NSLog(@"DEBUG: unsetReverseRelationship value class: %@, key: %@", [value class], key); // find relationship CAYObjEntity *objEntity = [[[self objectContext] entityResolver] lookupObjEntity:self]; CAYObjRelationship *relationship = [[objEntity relationships] valueForKey:key]; // find reverse relationship. could be nil. CAYObjEntity *revObjEntity = [[[self objectContext] entityResolver] lookupObjEntity:value]; CAYObjRelationship *revRelationship = [[revObjEntity relationships] valueForKey:[relationship reverseRelationshipName]]; if(revRelationship) { if([revRelationship isToMany]) { [value removeToManyTarget:self forKey:[revRelationship name] setReverse:NO]; } else { [value setToOneTarget:nil forKey:[revRelationship name] setReverse:NO]; } } } -(void)willConnect:(CAYPersistentObject *)value forKey:(NSString *)key { if(!value) { return; } if([self objectContext] && ![value objectContext]) { [[self objectContext] registerNewObject:value]; } else if([value objectContext] && ![self objectContext]) { [[value objectContext] registerNewObject:self]; } // TODO: give error if different data contexts? } -(void)awakeFromInsert { // init values with NSNull objects CAYObjEntity *objEntity = [[[self objectContext] entityResolver] lookupObjEntity:self]; NSEnumerator *enumerator = [[objEntity attributes] keyEnumerator]; NSString *attribute; while(attribute = [enumerator nextObject]) { if(![[self valuesRaw] objectForKey:attribute]) { NSLog(@"DEBUG: setting attribute %@ to NSNull", attribute); [[self valuesRaw] setObject:[NSNull null] forKey:attribute]; } } // init relationships enumerator = [[objEntity relationships] keyEnumerator]; NSString *relationshipName; while(relationshipName = [enumerator nextObject]) { if(![[self valuesRaw] objectForKey:relationshipName]) { CAYObjRelationship *relationship = [[objEntity relationships] objectForKey:relationshipName]; if([relationship isToMany]) { NSLog(@"DEBUG: setting relationship %@ to empty array", relationship); NSMutableArray *array = [[NSMutableArray alloc] init]; [[self valuesRaw] setObject:array forKey:relationshipName]; [array release]; } else { NSLog(@"DEBUG: setting attribute %@ to NSNull", relationship); [[self valuesRaw] setObject:[NSNull null] forKey:relationshipName]; } } } } -(void)dealloc { [self setObjectId:nil]; [self setObjectContext:nil]; [values release]; values = nil; [super dealloc]; } @end @implementation CAYPersistentObject (PrivateMethods) -(void)createValidationError:(NSString *)errorMessage code:(NSInteger)errorCode error:(NSError **)outError { NSDictionary *userInfoDict = [NSDictionary dictionaryWithObject:errorMessage forKey:NSLocalizedDescriptionKey]; NSError *error = [[[NSError alloc] initWithDomain:[[self class] description] code:errorCode userInfo:userInfoDict] autorelease]; *outError = error; } -(NSUndoManager *)undoManager { return [[self objectContext] undoManager]; } @end