/******************************************************************************* Copyright (c) 2001-2009, Perforce Software, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL PERFORCE SOFTWARE, INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *******************************************************************************/ /*===================================================================\ | Name : P4ClientApi.mm | | Author : Michael Bishop | | Description: Objective-C wrapper for the Perforce API. \===================================================================*/ #import "P4ClientApi.h" #import "P4ClientApiPriv.h" #import "i18napi.h" #import "p4tags.h" /*===================================================================\ | | | PUBLIC STRING CONSTANTS | | | \===================================================================*/ static NSString * StringConstant( const char * ); NSString * const P4ErrorDomain = @"com.perforce.p4.ErrorDomain"; NSString * const P4SubsystemErrorKey = @"P4ErrorSubsystem"; NSString * const P4SubCodeErrorKey = @"P4ErrorSubCode"; NSString * const P4SeverityErrorKey = @"P4ErrorSeverity"; NSString * const P4GenericErrorKey = @"P4ErrorGeneric"; NSString * const P4ProtocolVersion = StringConstant(P4Tag::v_api); /*===================================================================\ | | | INTERNAL PRIVATE METHODS AND PROPERTIES | | | \===================================================================*/ @interface P4ClientApi () -(void)P4_setArguments:(NSArray*)args forClientApi:(ClientApi*)api; -(void)P4_observeCallbackThread; -(void)P4_unobserveCallbackThread; -(void)P4_threadWillExit:(NSNotification*)notification; @property(readonly) ClientApi * api; // Make the publicly read-only properties privately writable @property(readwrite, copy) NSString * p4port; @property(readwrite, copy) NSDictionary * requestedProtocol; @property(readwrite) BOOL connected; @property(readwrite, copy) NSString * currentCommand; @property(readwrite, copy) NSArray * currentArguments; @end /*===================================================================\ | | | IMPLEMENTATION | | | \===================================================================*/ @implementation P4ClientApi @synthesize connected = _connected , currentCommand = _currentCommand , currentArguments = _currentArgs , userInfo = _userInfo , callbackThread = _callbackThread , ticketFilePath = _ticketFilePath , programIdentifier = _programIdentifier , version = _version , requestedProtocol = _requestedProtocol , prefersTaggedOutput = _prefersTaggedOutput , rowScanningLimit = _rowScanningLimit , tableLockingTimeLimit = _tableLockingTimeLimit , returnedResultsLimit = _returnedResultsLimit ; -(id)init { if ( (self = [super init]) == nil ) return self; _clientApi = new ClientApi(); _prefersTaggedOutput = YES; return self; } -(void)dealloc { // we might be dealloced in response to delegates receiving // a callback so we need to zeroout all the data as well as release it // because there might be code remaining on the stack that might want to // send messages to these variables (like release) _userInfo = nil; _currentCommand = nil; _currentArgs = nil; _ticketFilePath = nil; _programIdentifier = nil; _version = nil; _requestedProtocol = nil; // thj: removing this in the face of ARC // [_userInfo release]; _userInfo = nil; // [_currentCommand release]; _currentCommand = nil; // [_currentArgs release]; _currentArgs = nil; // [_ticketFilePath release]; _ticketFilePath = nil; // [_programIdentifier release]; _programIdentifier = nil; // [_version release]; _version = nil; // [_requestedProtocol release]; _requestedProtocol = nil; [self disconnect:nil]; delete self.api; // thj: removing this in the face of ARC // [super dealloc]; } -(ClientApi*)api { return (ClientApi*)_clientApi; } -(ClientUserWrapper*)currentClientUser { return (ClientUserWrapper*)_currentClientUser; } #pragma mark making/breaking the connection /*-------------------------------------------------------------------\ | | | Connecting/Disconnecting | | | \-------------------------------------------------------------------*/ -(BOOL)connectToPort:(NSString*)port withProtocol:(NSDictionary*)protocol error:(NSError **)error { Error e; @synchronized (self) { self.p4port = port; self.requestedProtocol = protocol; self.api->Init(&e); if ( !e.Test() ) [self setConnected: YES]; } if ( !e.Test() ) return YES; if (error) *error = NSErrorFromError( &e ); return NO; } -(void)connectToPort:(NSString*)port withProtocol:(NSDictionary*)protocol delegate:(id)delegate { NSError * error; // we can only call back in another thread if we can use NSObject methods BOOL delegateIsAnNSObject = [delegate isKindOfClass:[NSObject class]]; if ( [self connectToPort:port withProtocol:protocol error:&error] == YES ) { if (!_callbackThread || !delegateIsAnNSObject ) { [delegate clientApiDidConnect:self]; return; } [(NSObject *)delegate performSelector:@selector(clientApiDidConnect:) onThread:_callbackThread withObject:self waitUntilDone:YES]; return; } if (!_callbackThread || !delegateIsAnNSObject) { [delegate clientApi:self didFailToConnectWithError:error]; return; } SEL sel = @selector(clientApi:didFailToConnectWithError:); NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[(NSObject *)delegate methodSignatureForSelector:sel]]; [invocation setTarget:delegate]; [invocation setSelector:sel]; [invocation setArgument:(void*)&self atIndex:2]; [invocation setArgument:&error atIndex:3]; [invocation performSelector:@selector(invoke) onThread:_callbackThread withObject:nil waitUntilDone:YES]; } -(NSInvocation*)invocationForConnectToPort:(NSString*)port withProtocol:(NSDictionary*)protocol delegate:(id)delegate { const SEL runCommandSelector = @selector(connectToPort:withProtocol:delegate:); NSInvocation * invocation = [NSInvocation invocationWithMethodSignature: [self methodSignatureForSelector:runCommandSelector]]; [invocation setSelector:runCommandSelector]; [invocation setTarget:self]; if ( port ) [invocation setArgument:&port atIndex:2]; if ( protocol ) [invocation setArgument:&protocol atIndex:3]; if ( delegate ) [invocation setArgument:&delegate atIndex:4]; return invocation; } -(BOOL)disconnect:(NSError **)error { Error e; @synchronized (self) { if ( !self.connected ) return YES; self.api->Final(&e); [self setConnected:NO]; } if ( !e.Test() ) return YES; if (error) *error = NSErrorFromError( &e ); return NO; } -(void)disconnectWithDelegate:(id)delegate { NSError * error; [self disconnect:&error]; // we can only call back in another thread if we can use NSObject methods BOOL delegateIsAnNSObject = [delegate isKindOfClass:[NSObject class]]; if ( !_callbackThread || !delegateIsAnNSObject ) [delegate clientApi:self didDisconnectWithError:error]; SEL sel = @selector(clientApi:didDisconnectWithError:); NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[(NSObject*)delegate methodSignatureForSelector:sel]]; [invocation setTarget:delegate]; [invocation setSelector:sel]; [invocation setArgument:(void*)&self atIndex:2]; [invocation setArgument:&error atIndex:3]; [invocation performSelector:@selector(invoke) onThread:_callbackThread withObject:nil waitUntilDone:YES]; } -(NSInvocation*)invocationForDisconnectWithDelegate:(id)delegate { SEL selector = @selector(disconnectWithDelegate:); NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; [invocation setTarget:self]; [invocation setSelector:selector]; if ( delegate ) [invocation setArgument:&delegate atIndex:2]; return invocation; } #pragma mark executing commands /*===================================================================\ | | | Executing Commands | | | \===================================================================*/ -(void)P4_setArguments:(NSArray*)args forClientApi:(ClientApi*)api { if (args == nil || [args count] == 0) return; char ** a = (char**)malloc( [args count] * sizeof(char *) ); int i = 0; for (NSString * arg in args) a[i++] = const_cast([arg UTF8String]); api->SetArgv( i, a ); free( a ); } -(void)runCommand:(NSString *)function withArguments:(NSArray*)args delegate:(id)delegate { // set the per-command variables if ( self.programIdentifier ) self.api->SetProg([self.programIdentifier UTF8String]); if ( self.version ) self.api->SetVersion([self.version UTF8String]); if ( self.prefersTaggedOutput ) self.api->SetVar( P4Tag::v_tag, "" ); if ( self.rowScanningLimit > 0 ) self.api->SetVar( P4Tag::v_maxScanRows, self.rowScanningLimit ); if ( self.tableLockingTimeLimit > 0 ) self.api->SetVar( P4Tag::v_maxLockTime, self.tableLockingTimeLimit ); if ( self.returnedResultsLimit > 0 ) self.api->SetVar( P4Tag::v_maxResults, self.returnedResultsLimit ); self.currentCommand = function; self.currentArguments = args; [self P4_setArguments:args forClientApi:self.api]; ClientUserWrapper * wrapper = new ClientUserWrapper(self, delegate);; _currentClientUser = wrapper; self.api->Run([function UTF8String], wrapper ); _currentClientUser = NULL; delete wrapper; self.currentCommand = nil; self.currentArguments = nil; } -(NSInvocation*)invocationForRunCommand:(NSString *)command withArguments:(NSArray*)arguments delegate:(id)delegate { SEL runCommandSelector = @selector(runCommand:withArguments:delegate:); NSInvocation * invocation = [NSInvocation invocationWithMethodSignature: [self methodSignatureForSelector:runCommandSelector]]; [invocation setTarget:self]; [invocation setSelector:runCommandSelector]; if ( !command ) return nil; [invocation setArgument:&command atIndex:2]; if ( arguments ) [invocation setArgument:&arguments atIndex:3]; if ( delegate ) [invocation setArgument:&delegate atIndex:4]; return invocation; } -(void)P4_observeCallbackThread { if ( !_callbackThread ) return; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(P4_threadWillExit:) name:NSThreadWillExitNotification object:_callbackThread]; } -(void)P4_unobserveCallbackThread { if ( !_callbackThread ) return; [[NSNotificationCenter defaultCenter] removeObserver:self name:NSThreadWillExitNotification object:_callbackThread]; } -(void)P4_threadWillExit:(NSNotification*)notification { [self P4_unobserveCallbackThread]; self.callbackThread = nil; } #pragma mark properties /*===================================================================\ | | | Properties | | | \===================================================================*/ -(NSString*)p4port { return [NSString stringWithUTF8String:self.api->GetPort().Text()]; } -(void)setP4port:(NSString *)s { if ( s == nil ) return; self.api->SetPort([s UTF8String]); } -(void)setRequestedProtocol:(NSDictionary *)protocol { if ( _requestedProtocol == protocol ) return; // thj removing in the face of ARC // [_requestedProtocol release]; _requestedProtocol = [protocol copy]; for (NSString * key in _requestedProtocol) { NSObject * obj = [_requestedProtocol objectForKey:key]; const char * utf8Key = [key UTF8String]; if ( obj == [NSNull null] ) { self.api->SetProtocol(utf8Key, ""); } else if ( [obj isKindOfClass:[NSString class]] ) { self.api->SetProtocol(utf8Key, [(NSString*)obj UTF8String]); } else if ( [obj isKindOfClass:[NSNumber class]] ) { self.api->SetProtocol(utf8Key, [[(NSNumber*)obj stringValue] UTF8String]); } } } -(NSObject*)serverProtocolValueForKey:(NSString*)key { StrPtr * val = self.api->GetProtocol([key UTF8String]); if ( !val ) return nil; return StringFromUtf8StrPtr(val); } -(NSString*)user { return [NSString stringWithUTF8String:self.api->GetUser().Text()]; } -(void)setUser:(NSString *)s { if ( s == nil ) s = @""; self.api->SetUser([s UTF8String]); } -(NSString*)client { return [NSString stringWithUTF8String:self.api->GetClient().Text()]; } -(void)setClient:(NSString *)s { if ( s == nil ) s = @""; self.api->SetClient([s UTF8String]); } -(NSString*)charset { return [NSString stringWithUTF8String:self.api->GetCharset().Text()]; } -(void)setCharset:(NSString *)s { if ( s == nil ) { self.api->SetCharset(""); self.api->SetTrans( CharSetApi::NOCONV ); return; } const char * cString = [s UTF8String]; CharSetApi::CharSet cs = CharSetApi::Lookup( cString ); if ( cs == -1 ) return; self.api->SetCharset(cString); // We assume everything coming back from the server is in utf8 except // file content which we allow to be translated self.api->SetTrans( CharSetApi::UTF_8, cs, CharSetApi::UTF_8, CharSetApi::UTF_8 ); } -(NSString*)currentWorkingDirectory { return [NSString stringWithUTF8String:self.api->GetCwd().Text()]; } -(void)setCurrentWorkingDirectory:(NSString *)s { if ( s == nil ) s = @""; self.api->SetCwd([s UTF8String]); } -(NSString*)hostname { return [NSString stringWithUTF8String:self.api->GetHost().Text()]; } -(void)setHostname:(NSString *)s { if ( s == nil ) s = @""; self.api->SetHost([s UTF8String]); } -(NSString*)password { return [NSString stringWithUTF8String:self.api->GetPassword().Text()]; } -(void)setPassword:(NSString *)s { if ( s == nil ) s = @""; self.api->SetPassword([s UTF8String]); } -(void)setTicketFilePath:(NSString *)ticketFilePath { if ( ticketFilePath == _ticketFilePath ) return; // There is no getter in the C++ Client Api so we // keep a copy around for the getter // [_ticketFilePath autorelease]; _ticketFilePath = [ticketFilePath copy]; self.api->SetTicketFile([_ticketFilePath UTF8String]); } -(void)setCallbackThread:(NSThread *)thread { if ( thread == _callbackThread ) return; [self P4_unobserveCallbackThread]; // [_callbackThread autorelease]; // _callbackThread = [thread retain]; [self P4_observeCallbackThread]; } #pragma mark readonly properties -(NSString *)configurationFilename { return [NSString stringWithUTF8String:self.api->GetConfig().Text()]; } -(BOOL)connected { return ( _connected && !self.api->Dropped() ); } @end #pragma mark utility NSString * StringConstant( const char * cString ) { // NSStrings are toll-free bridges with CFStringRef return (NSString *)CFBridgingRelease(CFStringCreateWithCStringNoCopy( kCFAllocatorDefault, cString, kCFStringEncodingUTF8, kCFAllocatorDefault )); }