// // P4SpecManager.m // SCMMenuExtra // // Created by Michael Bishop on 1/29/10. // Copyright 2010 Perforce Software. All rights reserved. // #import "P4SpecManager.h" #import "P4SpecDescription.h" #import "P4Port.h" #import "P4Connection.h" #import "P4ConnectionPool.h" #import "P4Response.h" #import "P4ClientApi.h" #import "P4ErrorCodes.h" #import "NGAArrayAdditions.h" #import "NGAStringAdditions.h" #import "P4TaggedDataInflaterTransformer.h" #import "P4TaggedDataInflaterTransformer.h" #import "P4SpecManager_p.h" #import "P4SpecEntityDescriptionAdditions.h" #define COREDATA_STORE_TYPE NSInMemoryStoreType //NSString * const kP4SerializedPropertyKeys = @"P4SerializedPropertyKeys"; //NSString * const kP4DataBackedPropertyNameKey = @"P4DataBackedPropertyNameKey"; NSString * const kSpecPropertyNameDateRefreshed = @"lastRefreshDatesByProperty"; NSString * const kSpecPropertyNameType = @"type"; NSString * const kSpecPropertyNameIdentifier = @"identifier"; NSString * const kSpecPropertyNameBaseFieldValues = @"baseFields"; NSString * const kSpecPropertyNameTheirsFieldValues = @"theirsFields"; // 'Theirs' refers to Perforce terminology when performing a merge. NSString * const kEntityPropertyNameSpecSummarizedPropertyKeys = @"summarizedPropertyKeys"; NSString * const kP4LastAccessTimeKey = @"isLastAccessTime"; //NSString * const kSerializedDataKeyPostfix = @"AsSerializedData"; // MetaData strings NSString * const kP4FetchedRelationships = @"P4FetchedRelationships"; NSString * const kP4FetchedRelationshipDestinationSpecType = @"P4FetchedRelationshipDestinationSpecType"; NSString * const kP4FetchedRelationshipDestinationInverseName = @"P4FetchedRelationshipDestinationInverseName"; NSString * const kP4FetchedRelationshipName = @"P4FetchedRelationshipName"; NSString * const kP4InverseFetchedPropertyRelationshipName = @"P4InverseFetchedPropertyRelationshipName"; NSString * const kP4InverseFetchedPropertyRelationshipSpecType = @"P4InverseFetchedPropertyRelationshipSpecType"; NSString * const kNGAFetchedPropertyFetchDate = @"NGAFetchedPropertyFetchDate"; NSString * const kNGAFetchedPropertyFetchData = @"NGAFetchedPropertyFetchData"; @interface P4SpecManager () -(id)initWithP4Port:(NSString*)p username:(NSString*)u; -(NSString*)specCacheDirectory; -(NSManagedObjectModel *)managedObjectModel; -(NSManagedObjectContext *) managedObjectContext; -(NSString*)entityNameForTypeName:(NSString*)typeName; -(NSEntityDescription*)newEntityDescriptionForType:(NSString*)typeName error:(NSError**)error; -(NSArray*)filterArgumentsForListingSpecType:(NSString*)command fromPredicate:(NSPredicate*)predicate; @end struct defaultspec { NSString * const type; NSString * const spec; } speclist[] = { { @"branch", @"Branch;code:301;rq;ro;fmt:L;len:32;;" "Update;code:302;type:date;ro;fmt:L;len:20;;" "Access;code:303;type:date;ro;fmt:L;len:20;;" "Owner;code:304;fmt:R;len:32;;" "Description;code:306;type:text;len:128;;" "Options;code:309;type:line;len:32;val:" "unlocked/locked;;" "View;code:311;type:wlist;words:2;len:64;;" }, { @"change", @"Change;code:201;rq;ro;fmt:L;seq:1;len:10;;" "Date;code:202;type:date;ro;fmt:R;seq:3;len:20;;" "Client;code:203;ro;fmt:L;seq:2;len:32;;" "User;code:204;ro;fmt:L;seq:4;len:32;;" "Status;code:205;ro;fmt:R;seq:5;len:10;;" "Description;code:206;type:text;rq;seq:6;;" "JobStatus;code:207;fmt:I;type:select;seq:8;;" "Jobs;code:208;type:wlist;seq:7;len:32;;" "Files;code:210;type:llist;len:64;;" }, { @"client", @"Client;code:301;rq;ro;seq:1;len:32;;" "Update;code:302;type:date;ro;seq:2;fmt:L;len:20;;" "Access;code:303;type:date;ro;seq:4;fmt:L;len:20;;" "Owner;code:304;seq:3;fmt:R;len:32;;" "Host;code:305;seq:5;fmt:R;len:32;;" "Description;code:306;type:text;len:128;;" "Root;code:307;rq;type:line;len:64;;" "AltRoots;code:308;type:llist;len:64;;" "Options;code:309;type:line;len:64;val:" "noallwrite/allwrite,noclobber/clobber,nocompress/compress," "unlocked/locked,nomodtime/modtime,normdir/rmdir;;" "SubmitOptions;code:313;type:select;fmt:L;len:25;val:" "submitunchanged/submitunchanged+reopen/revertunchanged/" "revertunchanged+reopen/leaveunchanged/leaveunchanged+reopen;;" "LineEnd;code:310;type:select;fmt:L;len:12;val:" "local/unix/mac/win/share;;" "View;code:311;type:wlist;words:2;len:64;;" }, { @"depot", @"Depot;code:251;rq;ro;len:32;;" "Owner;code:252;len:32;;" "Date;code:253;type:date;ro;len:20;;" "Description;code:254;type:text;len:128;;" "Type;code:255;rq;len:10;;" "Address;code:256;len:64;;" "Suffix;code:258;len:64;;" "Map;code:257;rq;len:64;;" }, { @"group", @"Group;code:401;rq;ro;len:32;;" "MaxResults;code:402;type:word;len:12;;" "MaxScanRows;code:403;type:word;len:12;;" "MaxLockTime;code:407;type:word;len:12;;" "Timeout;code:406;type:word;len:12;;" "Subgroups;code:404;type:wlist;len:32;opt:default;;" "Owners;code:408;type:wlist;len:32;opt:default;;" "Users;code:405;type:wlist;len:32;opt:default;;" }, { @"job", @"Job;code:101;rq;len:32;;" "Status;code:102;type:select;rq;len:10;" "pre:open;val:open/suspended/closed;;" "User;code:103;rq;len:32;pre:$user;;" "Date;code:104;type:date;ro;len:20;pre:$now;;" "Description;code:105;type:text;rq;pre:$blank;;" }, { @"label", @"Label;code:301;rq;ro;fmt:L;len:32;;" "Update;code:302;type:date;ro;fmt:L;len:20;;" "Access;code:303;type:date;ro;fmt:L;len:20;;" "Owner;code:304;fmt:R;len:32;;" "Description;code:306;type:text;len:128;;" "Options;code:309;type:line;len:64;val:" "unlocked/locked;;" "Revision;code:312;type:word;words:1;len:64;;" "View;code:311;type:wlist;len:64;;" }, { @"license", @"License;code:451;len:32;;" "License-Expires;code:452;len:10;;" "Support-Expires;code:453;len:10;;" "Customer;code:454;type:line;len:128;;" "Application;code:455;len:32;;" "IPaddress;code:456;len:24;;" "Platform;code:457;len:32;;" "Clients;code:458;len:8;;" "Users;code:459;len:8;;" }, { @"protect", @"Protections;code:501;type:wlist;words:5;opt:default;len:64;;" }, { @"spec", @"Fields;code:351;type:wlist;words:5;rq;;" "Words;code:352;type:wlist;words:2;;" "Formats;code:353;type:wlist;words:3;;" "Values;code:354;type:wlist;words:2;;" "Presets;code:355;type:wlist;words:2;;" "Comments;code:356;type:text;;" }, { @"triggers", @"Triggers;code:551;type:wlist;words:4;len:64;opt:default;" }, { @"typemap", @"TypeMap;code:601;type:wlist;words:2;len:64;opt:default;" }, { @"user", @"User;code:651;rq;ro;seq:1;len:32;;" "Email;code:652;fmt:R;rq;seq:3;len:32;;" "Update;code:653;fmt:L;type:date;ro;seq:2;len:20;;" "Access;code:654;fmt:L;type:date;ro;len:20;;" "FullName;code:655;fmt:R;type:line;rq;len:32;;" "JobView;code:656;type:line;len:64;;" "Password;code:657;len:32;;" "Reviews;code:658;type:wlist;len:64;;" }, { nil, nil } }; // P4SpecManagers are retained by the application as a singleton static NSMutableDictionary * SpecManagerInstances = nil; @implementation P4SpecManager @synthesize connection; +(id)auxiliarySpecMetadata { static dispatch_once_t pred; static NSDictionary *sharedInstance = nil; dispatch_once(&pred, ^{ // IDEA: Add additional description information that a property of a spec // refers to a name (or names) of other specs. We can use this when // building our entity descriptions. NSString * pathToMetadataPropertyList = [[NSBundle bundleForClass:[self class]] pathForResource:@"P4AuxiliarySpecMetadata" ofType:@"plist"]; sharedInstance = [[NSDictionary dictionaryWithContentsOfFile:pathToMetadataPropertyList] retain]; if ( !sharedInstance ) NSLog(@"%@ not found! Fetched properties will not work!", pathToMetadataPropertyList); }); return sharedInstance; } +(NSArray*)allOpenManagers { return [SpecManagerInstances allValues]; } +(P4SpecManager*)managerWithConnection:(P4Connection*)connection { return [self managerWithPortString:connection.portString username:connection.username]; } +(P4SpecManager*)managerWithPortString:(NSString*)p4port username:(NSString*)username { if ( !SpecManagerInstances ) SpecManagerInstances = [NSMutableDictionary new]; NSString * key = [NSString stringWithFormat:@"%@/%@", p4port, username]; P4SpecManager * specManager = [SpecManagerInstances objectForKey:key]; if ( !specManager ) { specManager = [[[P4SpecManager alloc] initWithP4Port:p4port username:username] autorelease]; [SpecManagerInstances setObject:specManager forKey:key]; } return specManager; } -(id)initWithP4Port:(NSString*)p username:(NSString*)u { if ( (self = [super init]) == nil ) return nil; connection = [[P4Connection connectionWithPortString:p user:u] retain]; specDescriptions = [NSMutableDictionary new]; [self reset]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:NSApp]; return self; } -(id)init { return [self initWithP4Port:nil username:nil]; } -(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [specDescriptions release]; [connection release]; [managedObjectContext release]; [persistentStoreCoordinator release]; [managedObjectModel release]; [super dealloc]; } -(void)applicationWillTerminate:(NSNotification*)notification { [self flushCache:nil]; } -(void)refreshSpecDefinitionsWithCompletion:(SpecRefreshCompletionBlock)completion { __block int oustandingUpdateCount = 0; __block NSMutableArray * errors = [NSMutableArray array]; // There's probably a better way to do this, but I need to run a block with a // shared variable for each item in the spec list and then at the end, clean up // Thought: one way might be to assemble the blocks first and then to for( struct defaultspec *sp = &speclist[ 0 ]; sp->type; sp++ ) { NSString * typename = sp->type; NSArray * arguments = [NSArray arrayWithObjects:@"spec", @"-o", sp->type, nil]; [errors retain]; oustandingUpdateCount++; [self runArguments:arguments updateBlock:nil completionBlock:^(P4Response*response) { oustandingUpdateCount--; // These all happen on the main thread so we are ok changing this if ( response.error ) { [errors addObject:response.error]; [errors release]; return; } NSString * specdef = [self specDefStringFromSpecDefinition:response.result]; [self setSpecDefinitionAsString:specdef forTypeNamed:typename]; // lastly call the completion since we have fetched all the spec // definitions if (oustandingUpdateCount) { [errors release]; return; } NSError * error = nil; if ( [errors count] ) error = [NSError errorWithDomain:P4MEErrorDomain code:kP4MESpecDefinitionRefreshError userInfo:[NSDictionary dictionaryWithObject:errors forKey:kP4MEErrorUnderlyingErrorsKey] ]; completion( error ); [errors release]; }]; } } -(BOOL)flushCache:(NSError**)error { if ( ![[self managedObjectContext] hasChanges] ) return YES; return [[self managedObjectContext] save:error]; } -(void)save:(NSTimer*)timer { NSLog(@"saving..."); [savingTimer release]; savingTimer = nil; NSError * error = nil; if ( ![self flushCache:&error] ) { NSLog(@"%@", error); [NSApp presentError:error]; } // NSLog(@"...saved"); } -(void)requestSave { if ( ![[self managedObjectContext] hasChanges] ) return; // saves after a delay. The clock will restart if another request comes in const NSTimeInterval kSaveThrottle = 0.5; // If the save has been delayed longer than this, it will save regardless const NSTimeInterval kForcedSaveTimeout = 5.0; if ( !savingTimer ) { NSLog(@"...save requested..."); savingTimer = [[NSTimer scheduledTimerWithTimeInterval:kSaveThrottle target:self selector:@selector(save:) userInfo:[NSDate date] repeats:NO] retain]; } else { if ( [[NSDate date] timeIntervalSinceDate:[savingTimer userInfo]] > kForcedSaveTimeout ) { // NSLog( @"Seconds since first save request = %f. Forcing Save.", [[NSDate date] timeIntervalSinceDate:[savingTimer userInfo]] ); [savingTimer fire]; } else { [savingTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:kSaveThrottle]]; } } } -(void)reset { [specDescriptions removeAllObjects]; for( struct defaultspec *sp = &speclist[ 0 ]; sp->type; sp++ ) [self setSpecDefinitionAsString:sp->spec forTypeNamed:sp->type]; } -(BOOL)saveSpec:(P4Spec*)spec completionBlock:(void(^)(P4Response * response))completion { if ( !spec.isDirty ) { NSLog( @"Spec (%@:'%@') not dirty. Ignoring save request.", spec.type, spec.identifier ); return YES; } NSString * form = [spec form]; NSLog( @"Saving Form: %@", form ); return [self.connection runArguments:[NSArray arrayWithObjects:spec.type, @"-i", nil] withContext:[NSDictionary dictionaryWithObjectsAndKeys:form, kP4ConnectionContentContextKey, nil] updateBlock:nil completionBlock:completion]; } -(NSString*)entityNameForTypeName:(NSString*)typeName { return [typeName capitalizedString]; } -(NSString*)propertyNameForIdentifierPropertyForSpecType:(NSString*)typeName { NSEntityDescription * entityDescription = [NSEntityDescription entityForName:[self entityNameForTypeName:typeName ] inManagedObjectContext:[self managedObjectContext]]; NSString * propertyName = [[entityDescription userInfo] objectForKey:kP4IdentifierPropertyName]; return propertyName; } -(NSFetchRequest*)fetchRequestForType:(NSString*)typeName { NSFetchRequest * fetchRequest = [[[NSFetchRequest alloc] init] autorelease]; NSEntityDescription * entityDescription = [NSEntityDescription entityForName:[self entityNameForTypeName:typeName] inManagedObjectContext:[self managedObjectContext]]; [fetchRequest setEntity:entityDescription]; return fetchRequest; } -(NSFetchRequest*)fetchRequestForType:(NSString*)typeName identifier:(NSString*)identifier { NSFetchRequest * fetchRequest = [self fetchRequestForType:typeName]; [fetchRequest setFetchLimit:1]; NSString * identifierProperty = [self propertyNameForIdentifierPropertyForSpecType:typeName]; NSPredicate * predicate = [NSPredicate predicateWithFormat:@"%K == %@", identifierProperty, identifier]; [fetchRequest setPredicate:predicate]; return fetchRequest; } -(P4Spec*)newSpecWithType:(NSString*)typeName identifier:(NSString*)identifier { // Since it didn't exist, you can add it here // The returned object doesn't have anything loaded but when its values // are accessed, it will load from the server P4Spec * spec = [NSEntityDescription insertNewObjectForEntityForName:[self entityNameForTypeName:typeName ] inManagedObjectContext:[self managedObjectContext]]; // Here, we want to set the identifier without firing a change // Alternatively, we could construct and init one ourself [spec setPrimitiveValue:identifier forKey:[[spec entity] identifierPropertyName]]; return [spec retain]; } -(P4Spec*)specOfType:(NSString*)typeName identifier:(NSString*)identifier createIfNotFound:(BOOL)create { // First try to retrieve it from the cache if ( identifier == nil ) return nil; NSFetchRequest * fetchRequest = [self fetchRequestForType:typeName identifier:identifier]; if ( !fetchRequest ) return nil; NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; if ( !fetchResults ) { NSLog(@"%@", error); [NSApp presentError:error]; return nil; } if ( [fetchResults count] ) return [fetchResults objectAtIndex:0]; if ( create ) return [[self newSpecWithType:typeName identifier:identifier] autorelease]; return nil; } -(NSArray*)specListOfType:(NSString*)typeName completion:(SpeclistRefreshCompletionBlock)completion { NSFetchRequest * fetchRequest = [self fetchRequestForType:typeName]; [fetchRequest setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:[self propertyNameForIdentifierPropertyForSpecType:typeName] ascending:YES]]]; NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; NSDictionary * organizedResults = [fetchResults NGA_dictionaryByKeyingOnKey:kSpecPropertyNameIdentifier]; [self runArguments:[NSArray arrayWithObject:[self listCommandForSpecType:typeName]] updateBlock: ^(NSDictionary * specData) { NSString * identifier = [specData objectForKey:typeName]; if ( !identifier ) identifier = [specData objectForKey:[typeName capitalizedString]]; // Skip this one since there is nothing to identify it if ( !identifier ) return; P4Spec * spec = [organizedResults objectForKey:identifier]; if ( !spec ) spec = [[self newSpecWithType:typeName identifier:identifier] autorelease]; [spec updateWithRawData:specData]; } completionBlock:^(P4Response * response) { // First we organize the results so we may use them without // having to re-fetch the specs we are going to update // We organize them into a dictionary so we can reference them with // a fast lookup. I can only think there is a better way to do this NSLog(@"%lu results from fetch", (unsigned long)[organizedResults count]); NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; if ( !fetchResults ) { NSLog(@"%@", error); [NSApp presentError:error]; } completion( fetchResults, response.error ); // NSLog(@"Results from command: %@", fetchResults); }]; return fetchResults; } -(NSString*)listCommandForSpecType:(NSString*)specType { if ( [specType isEqualToString:@"branch"]) return [specType stringByAppendingString:@"es"]; return [specType stringByAppendingString:@"s"]; } -(NSArray*)filterArgumentsWhenListingType:(NSString*)typeName forComparisonPredicate:(NSComparisonPredicate*)compoundPredicate { NSExpression * leftExpression = [compoundPredicate leftExpression]; NSExpression * rightExpression = [compoundPredicate rightExpression]; if ( [leftExpression expressionType] != NSKeyPathExpressionType ) return nil; if ( [rightExpression expressionType] != NSConstantValueExpressionType ) return nil; NSString * keyPath = [leftExpression keyPath]; id value = [rightExpression constantValue]; if ( ![value isKindOfClass:[NSString class]] ) value = [value stringValue]; if ( !keyPath ) return nil; NSMutableArray * arguments = [NSMutableArray array]; BOOL hasNameFilter = NO; // It would be nice to replace this with a table if ([typeName isEqualToString:@"branch"]) { hasNameFilter = YES; if ( [keyPath isEqualToString:@"owner"] ) { [arguments addObject:@"-u"]; [arguments addObject:value]; } } else if ([typeName isEqualToString:@"change"]) { if ( [keyPath isEqualToString:@"user"] ) { [arguments addObject:@"-u"]; [arguments addObject:value]; } else if ( [keyPath isEqualToString:@"client"] ) { [arguments addObject:@"-c"]; [arguments addObject:value]; } } else if ([typeName isEqualToString:@"client"]) { hasNameFilter = YES; if ( [keyPath isEqualToString:@"owner"] && value ) { [arguments addObject:@"-u"]; [arguments addObject:value]; } } else if ([typeName isEqualToString:@"label"]) { hasNameFilter = YES; if ( [keyPath isEqualToString:@"owner"] ) { [arguments addObject:@"-u"]; [arguments addObject:[value stringValue]]; } } if ( hasNameFilter && ([keyPath isEqualToString:@"identifier"] || [keyPath isEqualToString:typeName] ) ) { [arguments addObject:@"-e"]; NSString * identifierSearchString = value; if ( NSEqualToPredicateOperatorType != [compoundPredicate predicateOperatorType] ) { if ( NSBeginsWithPredicateOperatorType == [compoundPredicate predicateOperatorType] ) identifierSearchString = [NSString stringWithFormat:@"%@*",identifierSearchString ]; if ( NSEndsWithPredicateOperatorType == [compoundPredicate predicateOperatorType] ) identifierSearchString = [NSString stringWithFormat:@"*%@",identifierSearchString ]; if ( NSContainsPredicateOperatorType == [compoundPredicate predicateOperatorType] ) identifierSearchString = [NSString stringWithFormat:@"*%@*",identifierSearchString ]; } [arguments addObject:identifierSearchString]; } return arguments; } -(NSString*)jobViewOperatorforPredicateOperatorType:(NSPredicateOperatorType)predicateOperatorType { switch (predicateOperatorType) { case NSLessThanPredicateOperatorType: return @"<"; case NSLessThanOrEqualToPredicateOperatorType: return @"<="; case NSGreaterThanPredicateOperatorType: return @">"; case NSGreaterThanOrEqualToPredicateOperatorType: return @">"; case NSEqualToPredicateOperatorType: return @"="; } return nil; } -(NSString*)jobViewStringfromPredicate:(NSPredicate*)predicate { // Our strategy must be to take this predicate and wherever we can, create // a filter that will return a superset of items this predicate matches. // They will be re-filtered later with [NSPredicate evaluateWithObject:] #if 0 Perforce job views: A job view is an expression that selects jobs according to word and date matches. Job views are used by the 'p4 jobs' -e flag to select which jobs to display. Also, the 'p4 user' form contains a JobView field which selects which jobs are to be presented during changelist creation for automatic closing upon changelist submission. 'p4 job' indexes all whitespace-separated words, and then any punctuation-separated words within those words. So 'sub-par' is entered into the index as 'sub', 'par', and 'sub-par'. Case is not considered. 'p4 job' separately indexes all date fields in a way that allows searching for a range of dates. In its simplest form, a job view can contain a list of words, separated by spaces, that must appear in a job for it to be selected. For a match to occur all words must appear somewhere in the job, not including date fields: JobView: GUI redrawing bug p4 jobs -e 'GUI redrawing bug' To select a particular field, the 'field=word' syntax may be used: JobView: GUI redrawing status=open Logical operators & (and), | (or), ^ (not), and () (grouping) may also be used; spaces remain a low-precedence 'and' operator: JobView: redrawing (type=bug|type=sir) status=open The ^ (not) operator cannot be used alone or with | (or), only in conjuction with & (and) or space (and): JobView: type=bug & ^status=closed Comparative operators >, >=, <, <=, and = are permitted. Because they succeed if any word in the field matches, only the = operator is useful against fields containing blocks of text: JobView: priority<=b description=gui Text searches may embed the wildcard *, which matches anything: JobView: redraw* type=bug To match operator characters, you can escape them with \. Date fields may be searched using comparative operators. Dates are of the form yyyy/mm/dd or yyyy/mm/dd:hh:mm:ss. If a specific time is not given, the equality operators (=, <=, >=) match the whole day: JobView: reported_date>=1998/01/01 status=closed Text field comparisons are done alphabetically. Date field comparisons are done chronologically. #endif NSMutableString * jobView = [NSMutableString string]; if ( [predicate isKindOfClass:[NSComparisonPredicate class]] ) { NSComparisonPredicate * comparisonPredicate = (NSComparisonPredicate*)predicate; NSExpression * leftExpression = [comparisonPredicate leftExpression]; if ( [leftExpression expressionType] != NSKeyPathExpressionType ) return nil; NSExpression * rightExpression = [comparisonPredicate rightExpression]; if ( [rightExpression expressionType] != NSConstantValueExpressionType ) return nil; NSString * keyPath = [leftExpression keyPath]; if ( !keyPath ) return nil; id value = [rightExpression constantValue]; if ( ![value isKindOfClass:[NSString class]] ) value = [value stringValue]; if ( !value ) return nil; NSString * operatorString = [self jobViewOperatorforPredicateOperatorType:[comparisonPredicate predicateOperatorType]]; if ( NSNotEqualToPredicateOperatorType == [comparisonPredicate predicateOperatorType] ) { [jobView appendString:@"^"]; operatorString = @"="; } [jobView appendString:keyPath]; [jobView appendString:operatorString]; if ( NSEndsWithPredicateOperatorType == [comparisonPredicate predicateOperatorType] || NSContainsPredicateOperatorType == [comparisonPredicate predicateOperatorType] ) [jobView appendString:@"*"]; [jobView appendString:value]; if ( NSBeginsWithPredicateOperatorType == [comparisonPredicate predicateOperatorType] || NSContainsPredicateOperatorType == [comparisonPredicate predicateOperatorType] ) [jobView appendString:@"*"]; return jobView; } // The predicate must be either compound or comparison if ( ![predicate isKindOfClass:[NSCompoundPredicate class]] ) return jobView; NSCompoundPredicate * compoundPredicate = (NSCompoundPredicate*)predicate; BOOL atBeginning = YES; for (NSPredicate * subPredicate in [compoundPredicate subpredicates]) { if ( NSNotPredicateType == [compoundPredicate compoundPredicateType] ) [jobView appendString:@"^"]; [jobView appendFormat:@"(%@)", [self jobViewStringfromPredicate:subPredicate]]; if (atBeginning) { atBeginning = NO; if ( NSOrPredicateType == [compoundPredicate compoundPredicateType] ) [jobView appendString:@" | "]; else if ( NSAndPredicateType == [compoundPredicate compoundPredicateType] ) [jobView appendString:@" "]; } } return jobView; } -(NSArray*)filterArgumentsForListingSpecType:(NSString*)typeName fromPredicate:(NSPredicate*)predicate { NSMutableArray * arguments = nil; // Note that for jobs queries, we can specify a precise server-side search if ( [typeName isEqualToString:@"job"] ) { arguments = [NSMutableArray arrayWithObject:@"-e"]; NSString * jobView = [self jobViewStringfromPredicate:predicate]; [arguments addObject:jobView]; return arguments; } // NON-JOB SPECS if ( [predicate isKindOfClass:[NSComparisonPredicate class]] ) { return [self filterArgumentsWhenListingType:typeName forComparisonPredicate:(NSComparisonPredicate*)predicate]; } // COMPOUND PREDICATES if ( ![predicate isKindOfClass:[NSCompoundPredicate class]] ) return nil; NSCompoundPredicate * compoundPredicate = (NSCompoundPredicate*)predicate; // We make sure all the top-level // items are ANDs and after that, create filters for only the top-level // items. So, the server will fetch more than this predicate requests, but // that's okay because the results will be post-processed through the // predicate before being returned to the client if ( NSAndPredicateType != [compoundPredicate compoundPredicateType] ) return nil; for (NSPredicate * subPredicate in [compoundPredicate subpredicates]) { if (!arguments) arguments = [NSMutableArray array]; if ( [subPredicate isKindOfClass:[NSComparisonPredicate class]] ) [arguments addObjectsFromArray:[self filterArgumentsWhenListingType:typeName forComparisonPredicate:(NSComparisonPredicate*)predicate]]; } return arguments; } -(NSArray*)executeFetchRequest:(NSFetchRequest*)fetchRequest incrementalUpdateBlock:(void(^)(NSArray * speclist))updateBlock completionBlock:(void(^)(NSArray * speclist, NSError *))completionBlock { NSString * typeName = [[fetchRequest entity] type]; if ( !typeName ) return nil; NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; NSDictionary * organizedResults = [fetchResults NGA_dictionaryByKeyingOnKey:kSpecPropertyNameIdentifier]; NSString * listCommand = [self listCommandForSpecType:typeName]; NSArray * filterArguments = [self filterArgumentsForListingSpecType:typeName fromPredicate:[fetchRequest predicate]]; NSArray * arguments = [[NSArray arrayWithObject:listCommand] arrayByAddingObjectsFromArray:filterArguments]; NSLog(@"arguments from fetchRequest: %@", arguments); [self runArguments:arguments updateBlock:^(NSDictionary * specData) { NSString * identifier = [specData objectForKey:typeName]; if ( !identifier ) identifier = [specData objectForKey:[typeName capitalizedString]]; // Skip this one since there is nothing to identify it if ( !identifier ) return; P4Spec * spec = [organizedResults objectForKey:identifier]; if ( !spec ) spec = [[self newSpecWithType:typeName identifier:identifier] autorelease]; [spec updateWithRawData:specData]; if ( updateBlock && [[fetchRequest predicate] evaluateWithObject:spec] ) { updateBlock( [NSArray arrayWithObject:spec] ); } } completionBlock:^(P4Response * response) { // First we organize the results so we may use them without // having to re-fetch the specs we are going to update // We organize them into a dictionary so we can reference them with // a fast lookup. I can only think there is a better way to do this NSLog(@"%lu results from fetch", (unsigned long)[organizedResults count]); if (!completionBlock) return; NSError * error = nil; NSArray * fetchResults = [[self managedObjectContext] executeFetchRequest:fetchRequest error:&error]; if ( !fetchResults ) { NSLog(@"%@", error); [NSApp presentError:error]; } completionBlock( fetchResults, response.error ); // NSLog(@"Results from command: %@", fetchResults); }]; return fetchResults; } -(NSString*)specCacheDirectory { NSArray * paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); if ( !paths || [paths count] == 0 ) return nil; NSString * cachePath = [paths objectAtIndex:0]; cachePath = [cachePath stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]]; cachePath = [cachePath stringByAppendingPathComponent:self.portString]; cachePath = [cachePath stringByAppendingPathComponent:self.username]; return cachePath; } -(NSString*)classNameForSpecTypeName:(NSString*)specTypeName { if ( [specTypeName isEqualToString:@"client"] ) return @"P4Client"; if ( [specTypeName isEqualToString:@"user"] ) return @"P4User"; return NSStringFromClass([P4Spec class]); } -(NSDictionary*)entitiesWithFetchedPropertiesFromEntities:(NSDictionary*)entities { NSMutableDictionary * entitiesWithFetchedProperties = [[entities mutableCopy] autorelease]; for (NSString * entityName in [entitiesWithFetchedProperties allKeys]) { NSEntityDescription * entity = [entities objectForKey:entityName]; NSString * typeName = [[entity userInfo] objectForKey:kP4SpecTypeKey]; NSString * identifierPropertyName = [[entity userInfo] objectForKey:kP4IdentifierPropertyName]; NSArray * fetchedProperties = [[[[self class] auxiliarySpecMetadata] objectForKey:typeName] objectForKey:kP4FetchedRelationships]; for (NSDictionary * fetchedProperty in fetchedProperties) { NSString * fetchedRelationshipName = [fetchedProperty objectForKey:kP4FetchedRelationshipName]; if ( !fetchedRelationshipName ) { NSLog(@"No entry of %@ in the %@ for %@", kP4FetchedRelationshipName, kP4FetchedRelationships, typeName); continue; } NSString * fetchedSpecType = [fetchedProperty objectForKey:kP4FetchedRelationshipDestinationSpecType]; if ( !fetchedRelationshipName ) { NSLog(@"No entry of %@ in the %@ for %@", kP4FetchedRelationshipDestinationSpecType, kP4FetchedRelationships, typeName); continue; } NSString * inverseRelationshipName = [fetchedProperty objectForKey:kP4FetchedRelationshipDestinationInverseName]; if ( !fetchedRelationshipName ) { NSLog(@"No entry of %@ in the %@ for %@", kP4FetchedRelationshipDestinationInverseName, kP4FetchedRelationships, typeName); continue; } NSFetchedPropertyDescription * clientsPropertyDescription = [[[NSFetchedPropertyDescription alloc] init] autorelease]; [clientsPropertyDescription setName:fetchedRelationshipName]; NSFetchRequest * clientsFetchRequest = [[[NSFetchRequest alloc] init] autorelease]; [clientsFetchRequest setEntity:[entities objectForKey:[self entityNameForTypeName:fetchedSpecType]]]; [clientsFetchRequest setPredicate:[NSPredicate predicateWithFormat:@"%K=$FETCH_SOURCE.%K", inverseRelationshipName, identifierPropertyName]]; [clientsPropertyDescription setFetchRequest:clientsFetchRequest]; NSMutableDictionary * fetchedPropertyDescriptions = [[[entity fetchedPropertyDescriptions] mutableCopy] autorelease]; if ( !fetchedPropertyDescriptions ) fetchedPropertyDescriptions = [NSMutableDictionary dictionary]; [fetchedPropertyDescriptions setObject:clientsPropertyDescription forKey:[clientsPropertyDescription name]]; [entity setFetchedPropertyDescriptions:fetchedPropertyDescriptions]; // Now make a note of the inverse relationship for // notification when loading the inverse spec { NSMutableDictionary * inverseFetchProperty = [NSMutableDictionary dictionaryWithCapacity:2]; [inverseFetchProperty setObject:inverseRelationshipName forKey:kP4InverseFetchedPropertyRelationshipName]; [inverseFetchProperty setObject:typeName forKey:kP4InverseFetchedPropertyRelationshipSpecType]; NSEntityDescription * inverseEntity = [entitiesWithFetchedProperties objectForKey:[self entityNameForTypeName:fetchedSpecType]]; // Let's make sure that property is indexed for faster lookups NSPropertyDescription * inverseProperty = [[inverseEntity propertiesByName] objectForKey:inverseRelationshipName]; [inverseProperty setIndexed:YES]; NSMutableArray * inverseFetchedRelationships = [[[inverseEntity inverseFetchedProperties] mutableCopy] autorelease]; if (!inverseFetchedRelationships) inverseFetchedRelationships = [NSMutableArray array]; [inverseFetchedRelationships addObject:inverseFetchProperty]; [inverseEntity setInverseFetchedProperties:inverseFetchedRelationships]; } } [entitiesWithFetchedProperties setObject:entity forKey:entityName]; } return entitiesWithFetchedProperties; } -(NSManagedObjectModel *)managedObjectModel { if (managedObjectModel) return managedObjectModel; managedObjectModel = [[NSManagedObjectModel alloc] init]; NSMutableDictionary * specEntitiesByName = [NSMutableDictionary dictionaryWithCapacity:[specDescriptions count]]; for ( NSString * specTypeKey in specDescriptions ) { NSError * error = nil; NSEntityDescription * entityDescription = [self newEntityDescriptionForType:specTypeKey error:&error]; if ( !entityDescription ) { NSLog( @"Skipping processing specdef for type: %@ because of error: %@", specTypeKey, error ); } else { // NSLog( @"Created entity named '%@' for type: '%@' -- %@", [entityDescription name], specTypeKey, entityDescription ); [specEntitiesByName setObject:entityDescription forKey:[entityDescription name]]; [entityDescription release]; } } NSDictionary * specEntitiesWithFetchedPropertiesByName = [self entitiesWithFetchedPropertiesFromEntities:specEntitiesByName]; [managedObjectModel setEntities:[specEntitiesWithFetchedPropertiesByName allValues]]; // Make each spectype in its own configuration so it can be saved into separate cache files for ( NSString * specTypeKey in specDescriptions ) { NSEntityDescription * entityDescription = [specEntitiesWithFetchedPropertiesByName objectForKey:[self entityNameForTypeName:specTypeKey]]; [managedObjectModel setEntities:[NSArray arrayWithObject:entityDescription] forConfiguration:specTypeKey]; } return managedObjectModel; } /** Returns the persistent store coordinator for the application. This implementation will create and return a coordinator, having added the store for the application to it. (The directory for the store is created, if necessary.) */ - (NSPersistentStoreCoordinator *) persistentStoreCoordinator { if (persistentStoreCoordinator) return persistentStoreCoordinator; NSManagedObjectModel *mom = [self managedObjectModel]; if (!mom) { NSAssert(NO, @"Managed object model is nil"); NSLog(@"%@:%@ No model to generate a store from", [self class], NSStringFromSelector(_cmd)); return nil; } NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *specCacheDirectory = [self specCacheDirectory]; NSError *error = nil; if ( ![fileManager fileExistsAtPath:specCacheDirectory isDirectory:NULL] ) { if (![fileManager createDirectoryAtPath:specCacheDirectory withIntermediateDirectories:YES attributes:nil error:&error]) { NSAssert(NO, ([NSString stringWithFormat:@"Failed to create Spec Cache directory %@ : %@", specCacheDirectory,error])); NSLog(@"Error creating application support directory at %@ : %@",specCacheDirectory,error); return nil; } } persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom]; for ( struct defaultspec *sp = &speclist[ 0 ]; sp->type; sp++ ) { NSString * baseStoreName = sp->type; NSString * storeType = COREDATA_STORE_TYPE; NSString * storeName = nil; if ( [storeType isEqualToString:NSXMLStoreType] ) storeName = [baseStoreName stringByAppendingString:@".xml"]; else if ( [storeType isEqualToString:NSSQLiteStoreType] ) storeName = [baseStoreName stringByAppendingString:@".sql"]; else if ( [storeType isEqualToString:NSBinaryStoreType] ) storeName = [baseStoreName stringByAppendingString:@".bin"]; else if ( [storeType isEqualToString:NSInMemoryStoreType] ) storeName = [baseStoreName stringByAppendingString:@".ram"]; NSURL *url = [NSURL fileURLWithPath: [specCacheDirectory stringByAppendingPathComponent:storeName]]; if (![persistentStoreCoordinator addPersistentStoreWithType:storeType configuration:sp->type URL:url options:nil error:&error]){ NSLog(@"%@", error); [[NSApplication sharedApplication] presentError:error]; [persistentStoreCoordinator release], persistentStoreCoordinator = nil; return nil; } } return persistentStoreCoordinator; } /** Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.) */ - (NSManagedObjectContext *) managedObjectContext { if (managedObjectContext) return managedObjectContext; NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (!coordinator) { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; [dict setValue:@"Failed to initialize the store" forKey:NSLocalizedDescriptionKey]; [dict setValue:@"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey]; NSError *error = [NSError errorWithDomain:@"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict]; NSLog(@"%@", error); [[NSApplication sharedApplication] presentError:error]; return nil; } managedObjectContext = [[NSManagedObjectContext alloc] init]; [managedObjectContext setPersistentStoreCoordinator: coordinator]; return managedObjectContext; } -(void)setSpecDefinitionAsFieldDefinitions:(NSDictionary*)fieldDefinitions forTypeNamed:(NSString*)typeName { [self setSpecDefinitionAsString:[self specDefStringFromSpecDefinition:fieldDefinitions] forTypeNamed:typeName]; } -(void)setSpecDefinitionAsString:(NSString*)string forTypeNamed:(NSString*)typeName { [specDescriptions setObject:[P4SpecDescription specDescriptionWithEncodedDefinition:string] forKey:typeName]; } -(BOOL)runArguments:(NSArray*)arguments updateBlock:(UpdateBlock)update completionBlock:(void(^)(P4Response*))completion { return [self.connection runArguments:arguments withContext:nil updateBlock:update completionBlock:completion]; } -(NSPropertyDescription*)propertyDescriptionForSpecFieldDescription:(P4SpecFieldDescription*)fieldDescription { NSString * tag = fieldDescription.tag; NSAttributeDescription * propertyDescription = [[[NSAttributeDescription alloc] init] autorelease]; NSMutableDictionary * userInfo = [NSMutableDictionary dictionary]; [userInfo setObject:tag forKey:kP4PropertyDescriptionSpecTag]; // Check if element is a "last access time" element if ( [fieldDescription isDate] && [fieldDescription isAlwaysSet] ) [userInfo setObject:[NSNumber numberWithBool:YES] forKey:kP4LastAccessTimeKey]; NSString * propertyName = nil; if ( [tag compare:@"description" options:NSCaseInsensitiveSearch] == NSOrderedSame) propertyName = @"comment"; else propertyName = [tag NGA_stringByConvertingFirstWordToLowerCase]; NSCharacterSet * disallowedSQLCharacters = [NSCharacterSet characterSetWithCharactersInString:@"-"]; // remove all invalid SQL characters propertyName = [[propertyName componentsSeparatedByCharactersInSet:disallowedSQLCharacters] componentsJoinedByString:@""]; // TYPE if ( [fieldDescription isDate] ) { [propertyDescription setAttributeType:NSDateAttributeType]; } else if ( [fieldDescription isList] ) { [propertyDescription setAttributeType:NSTransformableAttributeType]; } else { [propertyDescription setAttributeType:NSStringAttributeType]; } // NAME [propertyDescription setName:propertyName]; [propertyDescription setUserInfo:userInfo]; // VALUES // Not sure if I need to implement this since the server is really the // system that sets all the values. Our cache simply reads the values from // that system and places them in our objects. This is the data model // for our *cache* objects which we modify. not a direct translation from // the server // switch (specElem->opt) // { // case SDO_REQUIRED: // [propertyDescription setOptional:NO]; // Not sure if I need to set a default here. We are receiving the defaults // from the server and I need to be able to set them client-side // case SDO_DEFAULT: // [propertyDescription setDefaultValue:StringFromUtf8StrPtr(specElem->GetPreset()) ]; // break; // } return propertyDescription; } -(NSEntityDescription*)newEntityDescriptionForType:(NSString*)typeName error:(NSError**)error { NSEntityDescription * entityDescription = [[NSEntityDescription alloc] init]; P4SpecDescription * specDefinition = [specDescriptions objectForKey:typeName]; [entityDescription setName:[self entityNameForTypeName:typeName]]; NSSet * specCodes = [specDefinition allCodes]; NSMutableArray * properties = [NSMutableArray arrayWithCapacity:[specCodes count]]; NSMutableDictionary * specToMOPropertiesDict = [NSMutableDictionary dictionaryWithCapacity:[specCodes count]]; NSMutableDictionary * userInfo = [NSMutableDictionary dictionaryWithCapacity:5]; for (NSNumber * code in [specDefinition allCodes]) { P4SpecFieldDescription * fieldDescription = [specDefinition fieldForCode:[code intValue]]; if ( !fieldDescription ) { NSLog( @"No field description loaded for code %d.", [code intValue] ); continue; } NSString * tag = fieldDescription.tag; NSPropertyDescription * propertyDescription = [self propertyDescriptionForSpecFieldDescription:fieldDescription]; // determine if this is the property which stores the last time this // spec was accessed on the server if ( [[propertyDescription userInfo] objectForKey:kP4LastAccessTimeKey] ) [userInfo setObject:[propertyDescription name] forKey:kP4LastAcccessPropertyName]; [specToMOPropertiesDict setObject:[propertyDescription name] forKey:tag]; // The very first property is always the ID property if ( [fieldDescription isIdentifier] ) { [userInfo setObject:[propertyDescription name] forKey:kP4IdentifierPropertyName]; [propertyDescription setIndexed:YES]; } [properties addObject:propertyDescription]; } // Some properties that come back in the dictionary data are summaries // of other properties. These are handled specially in updateFromRawData: // 'desc' is a summary property for 'Description' when loading data for // a changelist. The data for the summary property is used when the full // data is not available. Asking for the property value when there is only // summary data loaded will cause the full data to be loaded. NSDictionary * summaryProperties = [[[[self class] auxiliarySpecMetadata] objectForKey:typeName] objectForKey:kP4SpecSummaryProperties]; if ( summaryProperties ) { NSAttributeDescription * summarizedKeysProperty = [[[NSAttributeDescription alloc] init] autorelease]; [summarizedKeysProperty setName:kEntityPropertyNameSpecSummarizedPropertyKeys]; [properties addObject:summarizedKeysProperty]; [summarizedKeysProperty setAttributeType:NSTransformableAttributeType]; [userInfo setObject:summaryProperties forKey:kP4SpecSummaryProperties]; } // Some properties are aliases for other properties. For example, "time" // and "date" are interchangeable when loading change summaries NSDictionary * aliases = [[[[self class] auxiliarySpecMetadata] objectForKey:typeName] objectForKey:kP4SpecPropertyAliases]; if (aliases) [specToMOPropertiesDict addEntriesFromDictionary:aliases]; // --- SPEC MANAGEMENT PROPERTIES --- // Date refreshed. This is stored into the cache so when we reload the // spec, we won't refresh it until is older than the refresh rate. NSAttributeDescription * dateRefreshedProperty = [[[NSAttributeDescription alloc] init] autorelease]; [dateRefreshedProperty setName:kSpecPropertyNameDateRefreshed]; [dateRefreshedProperty setAttributeType:NSTransformableAttributeType]; [properties addObject:dateRefreshedProperty]; // Base fields. This stores the base versions of fields that have been edited. If // this property has contents, it means this spec is "dirty" and needs // to be sent to the server or reverted.. NSAttributeDescription * baseFieldsProperty = [[[NSAttributeDescription alloc] init] autorelease]; [baseFieldsProperty setName:kSpecPropertyNameBaseFieldValues]; [baseFieldsProperty setAttributeType:NSTransformableAttributeType]; [properties addObject:baseFieldsProperty]; // 'Theirs' fields. If we have a 'base' value (see above) that is subsequently // modified, we store the modification in the "theirs" field. Should we ever // need to merge the two, we have all three legs and can merge appropriately NSAttributeDescription * theirsFieldsProperty = [[[NSAttributeDescription alloc] init] autorelease]; [theirsFieldsProperty setName:kSpecPropertyNameTheirsFieldValues]; [theirsFieldsProperty setAttributeType:NSTransformableAttributeType]; [properties addObject:theirsFieldsProperty]; [entityDescription setProperties:properties]; [userInfo setObject:specToMOPropertiesDict forKey:kP4SpecKeyToMOKeyMapping]; [userInfo setObject:self forKey:kP4SpecManager]; [userInfo setObject:typeName forKey:kP4SpecTypeKey]; [userInfo setObject:specDefinition forKey:kP4SpecDescriptionKey]; [entityDescription setManagedObjectClassName:[self classNameForSpecTypeName:typeName]]; [entityDescription setUserInfo:userInfo]; return entityDescription; } -(NSString*)formOfTypeNamed:(NSString*)typeName fromDictionary:(NSDictionary*)dictionary error:(NSError**)error { P4SpecDescription * specDefinition = [specDescriptions objectForKey:typeName]; if ( !specDefinition ) { if ( error ) { NSString * localizedString = NSLocalizedString(@"Could not find spec description for type %@", @"Spec Description not found error description"); localizedString = [NSString stringWithFormat:localizedString, typeName]; NSDictionary * userInfo = [NSDictionary dictionaryWithObjectsAndKeys:localizedString, NSLocalizedDescriptionKey, nil]; *error = [NSError errorWithDomain:P4MEErrorDomain code:kP4MESpecDefinitionNotFound userInfo:userInfo]; } return nil; } return [specDefinition formFromSpecProperties:dictionary]; } - (NSString*)portString { return self.connection.portString; } - (P4Port*)p4Port { return self.connection.p4port; } - (NSString*)username { return self.connection.username; } -(NSString*)specDefStringFromSpecDefinition:(NSDictionary*)dict { P4SpecDescription * specDescription = [P4SpecDescription specDescriptionWithSpecDefProperties:dict]; return [specDescription encodedDefinition]; } @end