//// BXEntityDescription.m// BaseTen//// Copyright (C) 2006-2008 Marko Karppinen & Co. LLC.//// Before using this software, please review the available licensing options// by visiting http://basetenframework.org/licensing/ or by contacting// us at sales@karppinen.fi. Without an additional license, this software// may be distributed only in compliance with the GNU General Public License.////// This program is free software; you can redistribute it and/or modify// it under the terms of the GNU General Public License, version 2.0,// as published by the Free Software Foundation.//// This program 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; if not, write to the Free Software// Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA//// $Id$//#import "BXEntityDescription.h"#import "BXEntityDescriptionPrivate.h"#import "BXDatabaseContext.h"#import "BXAttributeDescription.h"#import "BXRelationshipDescription.h"#import "BXRelationshipDescriptionPrivate.h"#import "BXAttributeDescriptionPrivate.h"#import "BXPropertyDescriptionPrivate.h"#import "BXDatabaseObject.h"#import "BXConstantsPrivate.h"#import "BXLogger.h"#import "BXWeakNotification.h"#import "PGTSHOM.h"#import "PGTSCollections.h"#import "BXEnumerate.h"#import "NSURL+BaseTenAdditions.h"/** * \brief An entity description contains information about a specific table or view in a given database. * * Only one entity description instance is created for a combination of a database * URI, a schema and a table. * * \note This class's documented methods are thread-safe. Creating objects, however, is not. * \note For this class to work in non-GC applications, the corresponding database context must be retained as well. * \ingroup descriptions */@implementationBXEntityDescription-(void)dealloc{[mDatabaseURIrelease];[mSchemaNamerelease];[mAttributesrelease];[mValidationLockrelease];[mObjectIDsrelease];[mRelationshipsrelease];[superdealloc];}/** * \brief The schema name. */-(NSString*)schemaName{return[[mSchemaNameretain]autorelease];}/** * \brief The database URI. */-(NSURL*)databaseURI{return[[mDatabaseURIretain]autorelease];}-(id)initWithCoder:(NSCoder*)decoder{NSString*name=[decoderdecodeObjectForKey:@"name"];NSString*schemaName=[decoderdecodeObjectForKey:@"schemaName"];NSURL*databaseURI=[decoderdecodeObjectForKey:@"databaseURI"];if((self=[selfinitWithDatabaseURI:databaseURItable:nameinSchema:schemaName])){Classcls=NSClassFromString([decoderdecodeObjectForKey:@"databaseObjectClassName"]);if(Nil!=cls)[selfsetDatabaseObjectClass:cls];[selfsetAttributes:[decoderdecodeObjectForKey:@"attributes"]];}returnself;}-(void)encodeWithCoder:(NSCoder*)encoder{[encoderencodeObject:mNameforKey:@"name"];[encoderencodeObject:mSchemaNameforKey:@"schemaName"];[encoderencodeObject:mDatabaseURIforKey:@"databaseURI"];[encoderencodeObject:NSStringFromClass(mDatabaseObjectClass)forKey:@"databaseObjectClassName"];[encoderencodeObject:mAttributesforKey:@"attributes"];//FIXME: relationships as well?[superencodeWithCoder:encoder];}/** * \brief Retain on copy. */-(id)copyWithZone:(NSZone*)zone{return[selfretain];}-(BOOL)isEqual:(BXEntityDescription*)desc{BOOLretval=NO;if(self==desc)retval=YES;elseif([superisEqual:desc]){NSString*s1=[selfschemaName];NSString*s2=[descschemaName];if(![s1isEqualToString:s2])gotobail;NSURL*u1=[selfdatabaseURI];NSURL*u2=[selfdatabaseURI];if(![u1isEqual:u2])gotobail;retval=YES;}bail:returnretval;}-(unsignedint)hash{returnmHash;}-(NSString*)description{return[NSStringstringWithFormat:@"<%@ %@ (%p) validated: %d enabled: %d>",mDatabaseURI,[selfname],self,[selfisValidated],[selfisEnabled]];}/** * \brief Set the class for this entity. * * Objects fetched using this entity will be instances of * the given class, which needs to be a subclass of BXDatabaseObject. * \param cls The object class. * \note If objects have been fetched from this entity before setting * a class, those objects might be returned by subsequent * fetches. It is best to set the class before connecting to the * database. */-(void)setDatabaseObjectClass:(Class)cls{if(YES==[clsisSubclassOfClass:[BXDatabaseObjectclass]]){@synchronized(self){mDatabaseObjectClass=cls;}}else{NSString*reason=[NSStringstringWithFormat:@"Expected %@ to be a subclass of BXDatabaseObject.",cls];[NSExceptionexceptionWithName:NSInternalInconsistencyExceptionreason:reasonuserInfo:nil];}}/** * \brief The class for this entity. * \return The default class is BXDatabaseObject. */-(Class)databaseObjectClass{idretval=nil;@synchronized(self){retval=mDatabaseObjectClass;}returnretval;}/** * \brief Registered object IDs for this entity. */-(NSArray*)objectIDs{idretval=nil;@synchronized(mObjectIDs){retval=[mObjectIDsallObjects];}returnretval;}staticintFilterPkeyAttributes(idattribute,void*arg){intretval=0;longshouldBePkey=(long)arg;if([attributeisPrimaryKey]==shouldBePkey)retval=1;returnretval;}/** * \brief Primary key fields for this entity. * * The fields get determined automatically after database connection has been made. * \return An array of BXAttributeDescriptions * \see #isValidated */-(NSArray*)primaryKeyFields{return[mAttributesPGTSValueSelectFunction:&FilterPkeyAttributesargument:(void*)1L]?:nil;}+(NSSet*)keyPathsForValuesAffectingFields{return[NSSetsetWithObject:@"primaryKeyFields"];}/** * \brief Non-primary key fields for this entity. * \return An array of BXAttributeDescriptions * \see #isValidated */-(NSArray*)fields{return[mAttributesPGTSValueSelectFunction:&FilterPkeyAttributesargument:(void*)0L]?:nil;}/** * \brief Whether this entity is marked as a view or not. */-(BOOL)isView{return(mFlags&kBXEntityIsView)?YES:NO;}-(NSComparisonResult)caseInsensitiveCompare:(BXEntityDescription*)anotherEntity{NSComparisonResultretval=NSOrderedSame;if(self!=anotherEntity){retval=[[selfschemaName]caseInsensitiveCompare:[anotherEntityschemaName]];if(NSOrderedSame==retval){retval=[[selfname]caseInsensitiveCompare:[anotherEntityname]];}}returnretval;}/** * \brief Attributes for this entity. * * Primary key fields and other fields for this entity. * \return An NSDictionary with NSStrings as keys and BXAttributeDescriptions as objects. * \see #isValidated */-(NSDictionary*)attributesByName{return[[mAttributesretain]autorelease];}/** * \brief Entity validation. * * The entity will be validated after a database connection has been made. Afterwards, * #fields, #primaryKeyFields, #attributesByName and #relationshipsByName return meaningful values. * * \note To call this safely, \em mValidationLock should be acquired first. Our validation methods do this, though. *///FIXME: should we have a separate variable for validation status?-(BOOL)isValidated{return(mFlags&kBXEntityIsValidated)?YES:NO;}/** * \brief Relationships for this entity. * \return An NSDictionary with NSStrings as keys and BXRelationshipDescriptions as objects. */-(NSDictionary*)relationshipsByName{if(![selfhasCapability:kBXEntityCapabilityRelationships])[NSExceptionraise:NSInvalidArgumentExceptionformat:@"Entity %@ doesn't have relationship capability. (BaseTen enabling is required for this.)",self];return[[mRelationshipsretain]autorelease];}-(BOOL)hasCapability:(enumBXEntityCapability)aCapability{return(mCapabilities&aCapability?YES:NO);}/** * \brief Whether this entity has been BaseTen enabled or not. */-(BOOL)isEnabled{return(mFlags&kBXEntityIsEnabled)?YES:NO;}-(void)viewGetsUpdatedWith:(NSArray*)entities{BXAssertVoidReturn([selfisView],@"Expected entity %@ to be a view.",self);[selfinherits:entities];}-(id)viewsUpdated{BXAssertValueReturn([selfisView],nil,@"Expected entity %@ to be a view.",self);return[selfinheritedEntities];}-(void)inherits:(NSArray*)entities{@synchronized(mSuperEntities){//FIXME: We only implement cascading notifications from "root tables" to inheriting tables and not vice-versa.//FIXME: only single entity supported for now.BXAssertVoidReturn(0==[mSuperEntitiescount],@"Expected inheritance/dependant relations not to have been set.");BXAssertVoidReturn(1==[entitiescount],@"Multiple inheritance/dependant relations is not supported.");BXEnumerate(currentEntity,e,[entitiesobjectEnumerator]){[mSuperEntitiesaddObject:currentEntity];[currentEntityaddSubEntity:self];}}}-(void)addSubEntity:(BXEntityDescription*)entity{@synchronized(mSubEntities){//FIXME: We only implement cascading notifications from "root tables" to inheriting tables and not vice-versa.[mSubEntitiesaddObject:entity];}}-(id)inheritedEntities{idretval=nil;@synchronized(mSuperEntities){retval=[mSuperEntitiesallObjects];}returnretval;}-(id)subEntities{idretval=nil;@synchronized(mSubEntities){retval=[mSubEntitiesallObjects];}returnretval;}/** * \internal * \brief Whether this entity gets changed by triggers, rules etc. * * If the entity gets changed only directly, some queries may possibly be optimized. */-(BOOL)getsChangedByTriggers{returnmFlags&kBXEntityGetsChangedByTriggers?YES:NO;}-(void)setGetsChangedByTriggers:(BOOL)flag{if(flag)mFlags|=kBXEntityGetsChangedByTriggers;elsemFlags&=~kBXEntityGetsChangedByTriggers;}@end@implementationBXEntityDescription(PrivateMethods)/** * \internal * \brief The designated initializer. * * Create the entity. * \param anURI The database URI * \param tName Table name * \param sName Schema name */-(id)initWithDatabaseURI:(NSURL*)anURItable:(NSString*)tNameinSchema:(NSString*)sName{BXAssertValueReturn(nil!=sName,nil,@"Expected sName not to be nil.");BXAssertValueReturn(nil!=anURI,nil,@"Expected anURI to be set.");if((self=[superinitWithName:tName])){mDatabaseObjectClass=[BXDatabaseObjectclass];mDatabaseURI=[anURIcopy];mSchemaName=[sNamecopy];mObjectIDs=PGTSSetCreateMutableWeakNonretaining();mSuperEntities=PGTSSetCreateMutableWeakNonretaining();mSubEntities=PGTSSetCreateMutableWeakNonretaining();mValidationLock=[[NSLockalloc]init];mHash=([superhash]^[mSchemaNamehash]^[mDatabaseURIBXHash]);}returnself;}-(id)init{[selfdoesNotRecognizeSelector:_cmd];returnnil;}-(id)initWithName:(NSString*)aName{[selfdoesNotRecognizeSelector:_cmd];returnnil;}//@}-(void)registerObjectID:(BXDatabaseObjectID*)anID{@synchronized(mObjectIDs){BXAssertVoidReturn([anIDentity]==self,@"Attempted to register an object ID the entity of which is other than self.\n""\tanID:\t%@ \n\tself:\t%@",anID,self);if(self==[anIDentity])[mObjectIDsaddObject:anID];}}-(void)unregisterObjectID:(BXDatabaseObjectID*)anID{@synchronized(mObjectIDs){[mObjectIDsremoveObject:anID];}}//Not thread-safe.-(void)setAttributes:(NSDictionary*)attributes{if(attributes!=mAttributes){[mAttributesrelease];mAttributes=[attributescopy];}}-(void)resetAttributeExclusion{[[mAttributesPGTSDo]resetAttributeExclusion];}-(NSArray*)attributes:(NSArray*)strings{return[mAttributesobjectsForKeys:stringsnotFoundMarker:[NSNullnull]];}-(void)setValidated:(BOOL)flag{if(flag)mFlags|=kBXEntityIsValidated;elsemFlags&=~kBXEntityIsValidated;}-(void)setIsView:(BOOL)flag{if(flag)mFlags|=kBXEntityIsView;elsemFlags&=~kBXEntityIsView;}//Not thread-safe.-(void)setRelationships:(NSDictionary*)aDict{if(mRelationships!=aDict){[mRelationshipsrelease];mRelationships=[aDictcopy];}}-(void)setHasCapability:(enumBXEntityCapability)aCapabilityto:(BOOL)flag{if(flag)mCapabilities|=aCapability;elsemCapabilities&=~aCapability;}-(void)setEnabled:(BOOL)flag{if(flag)mFlags|=kBXEntityIsEnabled;elsemFlags&=~kBXEntityIsEnabled;}staticintInverseToOneRelationships(idarg){intretval=0;BXRelationshipDescription*relationship=(BXRelationshipDescription*)arg;if([relationshipisInverse]&&![relationshipisToMany])retval=1;returnretval;}-(id)inverseToOneRelationships;{return[mRelationshipsPGTSValueSelectFunction:&InverseToOneRelationships];}-(BOOL)beginValidation{BOOLlocked=[mValidationLocktryLock];if(locked&&[selfisValidated]){[mValidationLockunlock];locked=NO;}returnlocked;}-(void)endValidation{if([selfhasCapability:kBXEntityCapabilityRelationships]){BXEnumerate(currentRelationship,e,[[selfrelationshipsByName]objectEnumerator])[currentRelationshipmakeAttributeDependency];}[mValidationLockunlock];}@end