/core/externals/update-engine/externals/gdata-objectivec-client/Source/BaseClasses/GDataServiceBase.m
Objective C | 2142 lines | 1417 code | 421 blank | 304 comment | 199 complexity | 97452e9420da4152311b1b41e2324812 MD5 | raw file
1/* Copyright (c) 2007 Google Inc. 2 * 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16// 17// GDataServiceBase.m 18// 19 20#import <TargetConditionals.h> 21#if TARGET_OS_MAC 22#include <sys/utsname.h> 23#endif 24 25#if TARGET_OS_IPHONE 26#import <UIKit/UIKit.h> 27#endif 28 29#define GDATASERVICEBASE_DEFINE_GLOBALS 1 30#import "GDataServiceBase.h" 31#import "GDataServerError.h" 32#import "GDataFramework.h" 33 34static NSString *const kXMLErrorContentType = @"application/vnd.google.gdata.error+xml"; 35 36static NSString* const kFetcherDelegateKey = @"_delegate"; 37static NSString* const kFetcherObjectClassKey = @"_objectClass"; 38static NSString* const kFetcherFinishedSelectorKey = @"_finishedSelector"; 39static NSString* const kFetcherCompletionHandlerKey = @"_completionHandler"; 40static NSString* const kFetcherTicketKey = @"_ticket"; 41static NSString* const kFetcherStreamDataKey = @"_streamData"; 42static NSString* const kFetcherParsedObjectKey = @"_parsedObject"; 43static NSString* const kFetcherParseErrorKey = @"_parseError"; 44static NSString* const kFetcherCallbackThreadKey = @"_callbackThread"; 45static NSString* const kFetcherCallbackRunLoopModesKey = @"_runLoopModes"; 46 47NSString* const kFetcherRetryInvocationKey = @"_retryInvocation"; 48 49static const NSUInteger kMaxNumberOfNextLinksFollowed = 25; 50 51// we'll enforce 50K chunks minimum just to avoid the server getting hit 52// with too many small upload chunks 53static const NSUInteger kMinimumUploadChunkSize = 50000; 54 55// XorPlainMutableData is a simple way to keep passwords held in heap objects 56// from being visible as plain-text 57static void XorPlainMutableData(NSMutableData *mutableData) { 58 59 // this helps avoid storing passwords on the heap in plaintext 60 const unsigned char theXORValue = 0x95; // 0x95 = 0xb10010101 61 62 unsigned char *dataPtr = [mutableData mutableBytes]; 63 NSUInteger length = [mutableData length]; 64 65 for (NSUInteger idx = 0; idx < length; idx++) { 66 dataPtr[idx] ^= theXORValue; 67 } 68} 69 70 71// category to provide opaque access to tickets stored in fetcher properties 72@implementation GTMHTTPFetcher (GDataServiceTicketAdditions) 73- (id)GDataTicket { 74 return [self propertyForKey:kFetcherTicketKey]; 75} 76@end 77 78// If GTMHTTPUploadFetcher is available, it can be used for chunked uploads 79// 80// We locally declare some methods of GTMHTTPUploadFetcher so we 81// do not need to import the header, as some projects may not have it available 82@interface GTMHTTPUploadFetcher : GTMHTTPFetcher 83+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request 84 uploadData:(NSData *)data 85 uploadMIMEType:(NSString *)uploadMIMEType 86 chunkSize:(NSUInteger)chunkSize 87 fetcherService:(GTMHTTPFetcherService *)fetcherService; 88+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request 89 uploadFileHandle:(NSFileHandle *)uploadFileHandle 90 uploadMIMEType:(NSString *)uploadMIMEType 91 chunkSize:(NSUInteger)chunkSize 92 fetcherService:(GTMHTTPFetcherService *)fetcherService; 93+ (GTMHTTPUploadFetcher *)uploadFetcherWithLocation:(NSURL *)locationURL 94 uploadFileHandle:(NSFileHandle *)uploadFileHandle 95 uploadMIMEType:(NSString *)uploadMIMEType 96 chunkSize:(NSUInteger)chunkSize 97 fetcherService:(GTMHTTPFetcherService *)fetcherService; 98- (void)pauseFetching; 99- (void)resumeFetching; 100- (BOOL)isPaused; 101@end 102 103@interface GDataEntryBase (PrivateMethods) 104- (NSDictionary *)contentHeaders; 105@end 106 107@interface GDataServiceBase (PrivateMethods) 108- (BOOL)fetchNextFeedWithURL:(NSURL *)nextFeedURL 109 delegate:(id)delegate 110 didFinishedSelector:(SEL)finishedSelector 111 completionHandler:(GDataServiceCompletionHandler)completionHandler 112 ticket:(GDataServiceTicketBase *)ticket; 113 114- (NSDictionary *)userInfoForErrorResponseData:(NSData *)data 115 contentType:(NSString *)contentType 116 previousUserInfo:(NSDictionary *)previousUserInfo; 117 118- (void)objectFetcher:(GTMHTTPFetcher *)fetcher 119 finishedWithData:(NSData *)data 120 error:(NSError *)error; 121- (void)objectFetcher:(GTMHTTPFetcher *)fetcher 122 failedWithData:(NSData *)data 123 error:(NSError *)error; 124- (BOOL)objectFetcher:(GTMHTTPFetcher *)fetcher 125 willRetry:(BOOL)willRetry 126 forError:(NSError *)error; 127- (void)objectFetcher:(GTMHTTPFetcher *)fetcher 128 didSendBytes:(NSInteger)bytesSent 129 totalBytesSent:(NSInteger)totalBytesSent 130totalBytesExpectedToSend:(NSInteger)totalBytesExpected; 131 132- (void)parseObjectFromDataOfFetcher:(GTMHTTPFetcher *)fetcher; 133- (void)handleParsedObjectForFetcher:(GTMHTTPFetcher *)fetcher; 134@end 135 136@implementation GDataServiceBase 137 138+ (Class)ticketClass { 139 return [GDataServiceTicketBase class]; 140} 141 142- (id)init { 143 self = [super init]; 144 if (self) { 145 146#if GDATA_IPHONE || (MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_5) 147 operationQueue_ = [[NSOperationQueue alloc] init]; 148#elif !GDATA_SKIP_PARSE_THREADING 149 // Avoid NSOperationQueue prior to 10.5.6, per 150 // http://www.mikeash.com/?page=pyblog/use-nsoperationqueue.html 151 SInt32 bcdSystemVersion = 0; 152 (void) Gestalt(gestaltSystemVersion, &bcdSystemVersion); 153 154 if (bcdSystemVersion >= 0x1057) { 155 operationQueue_ = [[NSOperationQueue alloc] init]; 156 } 157#else 158 // operationQueue_ defaults to nil, so parsing will be done immediately 159 // on the current thread 160#endif 161 162 fetcherService_ = [[GTMHTTPFetcherService alloc] init]; 163 [fetcherService_ setShouldRememberETags:YES]; 164 165 cookieStorageMethod_ = -1; 166 167 NSUInteger chunkSize = [[self class] defaultServiceUploadChunkSize]; 168 [self setServiceUploadChunkSize:chunkSize]; 169 } 170 return self; 171} 172 173- (void)dealloc { 174 [operationQueue_ release]; 175 176 [serviceVersion_ release]; 177 [userAgent_ release]; 178 [fetcherService_ release]; 179 180 [username_ release]; 181 [password_ release]; 182 183 [serviceUserData_ release]; 184 [serviceProperties_ release]; 185 [serviceSurrogates_ release]; 186 187#if NS_BLOCKS_AVAILABLE 188 [serviceUploadProgressBlock_ release]; 189#endif 190 191 [super dealloc]; 192} 193 194+ (NSString *)systemVersionString { 195 NSString *str = GTMSystemVersionString(); 196 return str; 197} 198 199- (NSString *)requestUserAgent { 200 201 NSString *userAgent = [self userAgent]; 202 203 if ([userAgent length] == 0 || [userAgent hasPrefix:@"MyCompany-"]) { 204 205 // the service instance is missing an explicit user-agent; use the bundle ID 206 // or process name 207 userAgent = [[self class] defaultApplicationIdentifier]; 208 } 209 210 NSString *requestUserAgent = userAgent; 211 212 // if the user agent already specifies the library version, we'll 213 // use it verbatim in the request 214 NSString *libraryString = @"GData-ObjectiveC"; 215 NSRange libRange = [userAgent rangeOfString:libraryString 216 options:NSCaseInsensitiveSearch]; 217 if (libRange.location == NSNotFound) { 218 // the user agent doesn't specify the client library, so append that 219 // information, and the system version 220 NSString *libVersionString = GDataFrameworkVersionString(); 221 222 NSString *systemString = [[self class] systemVersionString]; 223 224 // Google servers look for gzip in the user agent before sending gzip- 225 // encoded responses. See Service.java 226 requestUserAgent = [NSString stringWithFormat:@"%@ %@/%@ %@ (gzip)", 227 userAgent, libraryString, libVersionString, systemString]; 228 } 229 return requestUserAgent; 230} 231 232- (NSMutableURLRequest *)requestForURL:(NSURL *)url 233 ETag:(NSString *)etag 234 httpMethod:(NSString *)httpMethod 235 ticket:(GDataServiceTicketBase *)ticket { 236 237 // subclasses may add headers to this 238 NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] initWithURL:url 239 cachePolicy:NSURLRequestReloadIgnoringCacheData 240 timeoutInterval:60] autorelease]; 241 242 NSString *requestUserAgent = [self requestUserAgent]; 243 [request setValue:requestUserAgent forHTTPHeaderField:@"User-Agent"]; 244 245 NSString *serviceVersion = [self serviceVersion]; 246 if ([serviceVersion length] > 0) { 247 248 // only add a version header if the URL lacks a v= version parameter 249 NSString *queryString = [url query]; 250 if (queryString == nil 251 || ([queryString rangeOfString:@"&v="].location == NSNotFound 252 && ![queryString hasPrefix:@"v="])) { 253 254 [request setValue:serviceVersion forHTTPHeaderField:@"GData-Version"]; 255 } 256 } 257 258 if ([httpMethod length] > 0) { 259 [request setHTTPMethod:httpMethod]; 260 } 261 262 if ([etag length] > 0) { 263 264 // it's rather unexpected for an etagged object to be provided for a GET, 265 // but we'll check for an etag anyway, similar to HttpGDataRequest.java, 266 // and if present use it to request only an unchanged resource 267 268 BOOL isDoingHTTPGet = (httpMethod == nil 269 || [httpMethod caseInsensitiveCompare:@"GET"] == NSOrderedSame); 270 271 if (isDoingHTTPGet) { 272 273 // set the etag header, even if weak, indicating we don't want 274 // another copy of the resource if it's the same as the object 275 [request setValue:etag forHTTPHeaderField:@"If-None-Match"]; 276 277 } else { 278 279 // if we're doing PUT or DELETE, set the etag header indicating 280 // we only want to update the resource if our copy matches the current 281 // one (unless the etag is weak and so shouldn't be a constraint at all) 282 BOOL isWeakETag = [etag hasPrefix:@"W/"]; 283 284 BOOL isModifying = 285 [httpMethod caseInsensitiveCompare:@"PUT"] == NSOrderedSame 286 || [httpMethod caseInsensitiveCompare:@"DELETE"] == NSOrderedSame 287 || [httpMethod caseInsensitiveCompare:@"PATCH"] == NSOrderedSame; 288 289 if (isModifying && !isWeakETag) { 290 [request setValue:etag forHTTPHeaderField:@"If-Match"]; 291 } 292 } 293 } 294 295 return request; 296} 297 298- (NSMutableURLRequest *)requestForURL:(NSURL *)url 299 ETag:(NSString *)etag 300 httpMethod:(NSString *)httpMethod { 301 // this public entry point authenticates from the service object but 302 // not from the auth token in the ticket 303 return [self requestForURL:url ETag:etag httpMethod:httpMethod ticket:nil]; 304} 305 306// objectRequestForURL returns an NSMutableURLRequest for a GData object as XML 307// 308// the object is the object being sent to the server, or nil; 309// the http method may be nil for get, or POST, PUT, DELETE 310 311- (NSMutableURLRequest *)objectRequestForURL:(NSURL *)url 312 object:(GDataObject *)object 313 ETag:(NSString *)etag 314 httpMethod:(NSString *)httpMethod 315 ticket:(GDataServiceTicketBase *)ticket { 316 NSString *contentType = @"application/atom+xml; charset=utf-8"; 317 318 if (object) { 319 // if the object being sent has an etag, add it to the request header to 320 // avoid retrieving a duplicate or to avoid writing over an updated 321 // version of the resource on the server 322 // 323 // Typically, delete requests will provide an explicit ETag parameter, and 324 // other requests will have the ETag carried inside the object being updated 325 if (etag == nil) { 326 SEL selEtag = @selector(ETag); 327 if ([object respondsToSelector:selEtag]) { 328 etag = [object performSelector:selEtag]; 329 } 330 } 331 332 if (httpMethod != nil 333 && [httpMethod caseInsensitiveCompare:@"PATCH"] == NSOrderedSame) { 334 // PATCH isn't part of Atom 335 contentType = @"application/xml; charset=utf-8"; 336 } 337 } 338 339 NSMutableURLRequest *request = [self requestForURL:url 340 ETag:etag 341 httpMethod:httpMethod 342 ticket:ticket]; 343 344 [request setValue:@"application/atom+xml, text/xml" forHTTPHeaderField:@"Accept"]; 345 346 [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; 347 348 [request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"]; 349 350 return request; 351} 352 353#pragma mark - 354 355- (GDataServiceTicketBase *)fetchObjectWithURL:(NSURL *)feedURL 356 objectClass:(Class)objectClass 357 objectToPost:(GDataObject *)objectToPost 358 ETag:(NSString *)etag 359 httpMethod:(NSString *)httpMethod 360 delegate:(id)delegate 361 didFinishSelector:(SEL)finishedSelector 362 completionHandler:(id)completionHandler // GDataServiceCompletionHandler 363 retryInvocationValue:(NSValue *)retryInvocationValue 364 ticket:(GDataServiceTicketBase *)ticket { 365 366 GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, @encode(GDataServiceTicketBase *), @encode(GDataObject *), @encode(NSError *), 0); 367 368 // The completionHandler argument is declared as an id, not as a block 369 // pointer, so this can be built with the 10.6 SDK and still run on 10.5. 370 // If the argument were declared as a block pointer, the invocation for 371 // fetchObjectWithURL: created in GDataServiceGoogle would cause an exception 372 // since 10.5's NSInvocation cannot deal with encoding of block pointers. 373 374 NSURL *uploadLocationURL = [objectToPost uploadLocationURL]; 375 376 // if no URL was supplied, and we're not resuming an upload, treat this as 377 // if the fetch failed (below) and immediately return a nil ticket, skipping 378 // the callbacks 379 // 380 // this might be considered normal (say, updating a read-only entry 381 // that lacks an edit link) though higher-level calls may assert or 382 // returns errors depending on the specific usage 383 if (feedURL == nil && uploadLocationURL == nil) { 384 return nil; 385 } 386 387 // we need to create a ticket unless one was created earlier (like during 388 // authentication) 389 if (!ticket) { 390 ticket = [[[self class] ticketClass] ticketForService:self]; 391 } 392 393 NSMutableURLRequest *request = nil; 394 if (feedURL) { 395 request = [self objectRequestForURL:feedURL 396 object:objectToPost 397 ETag:etag 398 httpMethod:httpMethod 399 ticket:ticket]; 400 } 401 402 GTMAssertSelectorNilOrImplementedWithArgs(delegate, [ticket uploadProgressSelector], 403 @encode(GDataServiceTicketBase *), @encode(unsigned long long), 404 @encode(unsigned long long), 0); 405 GTMAssertSelectorNilOrImplementedWithArgs(delegate, [ticket retrySelector], 406 @encode(GDataServiceTicketBase *), @encode(BOOL), @encode(NSError *), 0); 407 408 // 409 // package the object's XML and any upload data 410 // 411 412 NSInputStream *streamToPost = nil; 413 NSData *dataToPost = nil; 414 SEL sentDataSel = NULL; 415 NSData *uploadData = nil; 416 NSFileHandle *uploadFileHandle = nil; 417 BOOL shouldUploadDataChunked = ([self serviceUploadChunkSize] > 0); 418 BOOL isUploadingDataChunked = NO; 419 420 NSMutableDictionary *uploadProperties = nil; 421 if (objectToPost) { 422 423 NSData *xmlData = nil; 424 425 [ticket setPostedObject:objectToPost]; 426 427 // An upload object may provide a custom input stream, such as for 428 // multi-part MIME or media uploads. 429 NSInputStream *contentInputStream = nil; 430 unsigned long long contentLength = 0; 431 NSDictionary *contentHeaders = nil; 432 433 uploadProperties = [NSMutableDictionary dictionary]; 434 435 BOOL doesSupportSentData = [GTMHTTPFetcher doesSupportSentDataCallback]; 436 437 uploadData = [objectToPost uploadData]; 438 uploadFileHandle = [objectToPost uploadFileHandle]; 439 440 isUploadingDataChunked = ((uploadData != nil || uploadFileHandle != nil) 441 && shouldUploadDataChunked); 442 BOOL shouldUploadDataOnly = ([objectToPost shouldUploadDataOnly] 443 || uploadLocationURL != nil); 444 445 BOOL shouldReportUploadProgress; 446#if NS_BLOCKS_AVAILABLE 447 shouldReportUploadProgress = ([ticket uploadProgressSelector] != NULL 448 || [ticket uploadProgressHandler] != NULL); 449#else 450 shouldReportUploadProgress = ([ticket uploadProgressSelector] != NULL); 451#endif 452 453 if ((!isUploadingDataChunked) && 454 [objectToPost generateContentInputStream:&contentInputStream 455 length:&contentLength 456 headers:&contentHeaders]) { 457 // now we have a stream containing the XML and the upload data 458 459 } else if (isUploadingDataChunked && shouldUploadDataOnly) { 460 // no XML is needed since we're uploading data only, and there's no upload 461 // data for the first fetch because the data will be sent chunked later, 462 // so now we'll have an empty body with appropriate headers 463 contentHeaders = [objectToPost performSelector:@selector(contentHeaders)]; 464 contentLength = 0; 465 466 } else { 467 // we're sending either just XML, or XML now with chunked upload data 468 // later 469 xmlData = [[objectToPost XMLDocument] XMLData]; 470 contentLength = [xmlData length]; 471 472 if (!shouldReportUploadProgress 473 || doesSupportSentData 474 || isUploadingDataChunked) { 475 // there is no progress selector, or the fetcher can call us back on 476 // sent data, or we're uploading chunked; we can post plain NSData, 477 // which is simpler because it survives http redirects 478 dataToPost = xmlData; 479 480 } else { 481 // there is a progress selector and NSURLConnection won't call us back, 482 // so we need to be posting a stream 483 // 484 // we'll make a default input stream from the XML data 485 contentInputStream = [NSInputStream inputStreamWithData:xmlData]; 486 487 // NSInputStream fails to retain or copy its data in 10.4, so we will 488 // retain it in the callback dictionary. We won't use this property in 489 // the callbacks at all, but retaining it will ensure it's still around 490 // until the upload completes. 491 // 492 // If it weren't for this bug in NSInputStream, we could just have 493 // GDataObject's -contentInputStream method create the input stream for 494 // us, so this service class wouldn't ever need to have the plain XML. 495 [uploadProperties setObject:xmlData forKey:kFetcherStreamDataKey]; 496 } 497 498 if ([objectToPost respondsToSelector:@selector(uploadSlug)]) { 499 NSString *slug = [objectToPost performSelector:@selector(uploadSlug)]; 500 if ([slug length] > 0) { 501 [request setValue:slug forHTTPHeaderField:@"Slug"]; 502 } 503 } 504 } 505 506 if (contentHeaders) { 507 // add the content-specific headers, if any 508 for (NSString *key in contentHeaders) { 509 NSString *value = [contentHeaders objectForKey:key]; 510 [request setValue:value forHTTPHeaderField:key]; 511 } 512 } 513 514 streamToPost = contentInputStream; 515 516 NSNumber* num = [NSNumber numberWithUnsignedLongLong:contentLength]; 517 [request setValue:[num stringValue] forHTTPHeaderField:@"Content-Length"]; 518 519 if (shouldReportUploadProgress) { 520 if (doesSupportSentData || isUploadingDataChunked) { 521 // there is sentData callback support in NSURLConnection, 522 // or we're using an upload fetcher which can always call us 523 // back 524 sentDataSel = @selector(objectFetcher:didSendBytes:totalBytesSent:totalBytesExpectedToSend:); 525 } 526 } 527 } 528 529 // 530 // now that we have all the request header info ready, 531 // create and set up the fetcher for this request 532 // 533 GTMHTTPFetcher* fetcher = nil; 534 535 if (isUploadingDataChunked) { 536 // hang on to the user's requested chunk size, and ensure it's not tiny 537 NSUInteger uploadChunkSize = [self serviceUploadChunkSize]; 538 if (uploadChunkSize < kMinimumUploadChunkSize) { 539 uploadChunkSize = kMinimumUploadChunkSize; 540 } 541 542#ifdef GDATA_TARGET_NAMESPACE 543 // prepend the class name prefix 544 Class uploadClass = NSClassFromString(@GDATA_TARGET_NAMESPACE_STRING 545 "_GTMHTTPUploadFetcher"); 546#else 547 Class uploadClass = NSClassFromString(@"GTMHTTPUploadFetcher"); 548#endif 549 GDATA_ASSERT(uploadClass != nil, @"GTMHTTPUploadFetcher needed"); 550 551 NSString *uploadMIMEType = [objectToPost uploadMIMEType]; 552 553 if (uploadData) { 554 fetcher = [uploadClass uploadFetcherWithRequest:request 555 uploadData:uploadData 556 uploadMIMEType:uploadMIMEType 557 chunkSize:uploadChunkSize 558 fetcherService:fetcherService_]; 559 } else if (uploadLocationURL) { 560 fetcher = [uploadClass uploadFetcherWithLocation:uploadLocationURL 561 uploadFileHandle:uploadFileHandle 562 uploadMIMEType:uploadMIMEType 563 chunkSize:uploadChunkSize 564 fetcherService:fetcherService_]; 565 } else { 566 fetcher = [uploadClass uploadFetcherWithRequest:request 567 uploadFileHandle:uploadFileHandle 568 uploadMIMEType:uploadMIMEType 569 chunkSize:uploadChunkSize 570 fetcherService:fetcherService_]; 571 } 572 } else { 573 fetcher = [fetcherService_ fetcherWithRequest:request]; 574 } 575 576 // allow the user to specify static app-wide cookies for fetching 577 NSInteger cookieStorageMethod = [self cookieStorageMethod]; 578 if (cookieStorageMethod >= 0) { 579 [fetcher setCookieStorageMethod:cookieStorageMethod]; 580 } 581 582 // copy the ticket's retry settings into the fetcher 583 [fetcher setRetryEnabled:[ticket isRetryEnabled]]; 584 [fetcher setMaxRetryInterval:[ticket maxRetryInterval]]; 585 586 if ([ticket retrySelector]) { 587 [fetcher setRetrySelector:@selector(objectFetcher:willRetry:forError:)]; 588 } 589 590 // remember the object fetcher in the ticket 591 [ticket setObjectFetcher:fetcher]; 592 [ticket setCurrentFetcher:fetcher]; 593 594 // add parameters used by the callbacks 595 596 [fetcher setProperty:objectClass forKey:kFetcherObjectClassKey]; 597 598 [fetcher setProperty:delegate forKey:kFetcherDelegateKey]; 599 600 [fetcher setProperty:NSStringFromSelector(finishedSelector) 601 forKey:kFetcherFinishedSelectorKey]; 602 603 [fetcher setProperty:ticket 604 forKey:kFetcherTicketKey]; 605 606#if NS_BLOCKS_AVAILABLE 607 // copy the completion handler block to the heap; this does nothing if the 608 // block is already on the heap 609 completionHandler = [[completionHandler copy] autorelease]; 610 [fetcher setProperty:completionHandler 611 forKey:kFetcherCompletionHandlerKey]; 612#endif 613 614 // we want to add the invocation itself, not the value wrapper of it, 615 // to ensure the invocation is retained until the callback completes 616 NSInvocation *retryInvocation = [retryInvocationValue nonretainedObjectValue]; 617 [fetcher setProperty:retryInvocation 618 forKey:kFetcherRetryInvocationKey]; 619 620 // set the upload data 621 GDATA_DEBUG_ASSERT(dataToPost == nil || streamToPost == nil, 622 @"upload conflict"); 623 [fetcher setPostData:dataToPost]; 624 [fetcher setPostStream:streamToPost]; 625 [fetcher setSentDataSelector:sentDataSel]; 626 [fetcher addPropertiesFromDictionary:uploadProperties]; 627 628 // attach OAuth authorization object, if any 629 // 630 // the fetcher already has this authorizer from the fetcher service, but this 631 // lets the client remove the authorizer from the ticket to make an 632 // unauthorized request 633 [fetcher setAuthorizer:[ticket authorizer]]; 634 635 // add username/password, if any 636 [self addAuthenticationToFetcher:fetcher]; 637 638 if (finishedSelector) { 639 [fetcher setComment:NSStringFromSelector(finishedSelector)]; 640 } 641 642 // failed fetches call the failure selector, which will delete the ticket 643 BOOL didFetch = [fetcher beginFetchWithDelegate:self 644 didFinishSelector:@selector(objectFetcher:finishedWithData:error:)]; 645 646 // If something weird happens and the networking callbacks have been called 647 // already synchronously, we don't want to return the ticket since the caller 648 // will never know when to stop retaining it, so we'll make sure the 649 // success/failure callbacks have not yet been called by checking the 650 // ticket 651 if (!didFetch || [ticket hasCalledCallback]) { 652 [fetcher setProperties:nil]; 653 [ticket setCurrentFetcher:nil]; 654 return nil; 655 } 656 657 return ticket; 658} 659 660- (void)invokeProgressCallbackForTicket:(GDataServiceTicketBase *)ticket 661 deliveredBytes:(unsigned long long)numReadSoFar 662 totalBytes:(unsigned long long)total { 663 664 SEL progressSelector = [ticket uploadProgressSelector]; 665 if (progressSelector) { 666 667 GTMHTTPFetcher *fetcher = [ticket objectFetcher]; 668 id delegate = [fetcher propertyForKey:kFetcherDelegateKey]; 669 670 NSMethodSignature *signature = [delegate methodSignatureForSelector:progressSelector]; 671 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; 672 673 [invocation setSelector:progressSelector]; 674 [invocation setTarget:delegate]; 675 [invocation setArgument:&ticket atIndex:2]; 676 [invocation setArgument:&numReadSoFar atIndex:3]; 677 [invocation setArgument:&total atIndex:4]; 678 [invocation invoke]; 679 } 680 681#if NS_BLOCKS_AVAILABLE 682 GDataServiceUploadProgressHandler block = [ticket uploadProgressHandler]; 683 if (block) { 684 block(ticket, numReadSoFar, total); 685 } 686#endif 687} 688 689// sentData callback from fetcher 690- (void)objectFetcher:(GTMHTTPFetcher *)fetcher 691 didSendBytes:(NSInteger)bytesSent 692 totalBytesSent:(NSInteger)totalBytesSent 693totalBytesExpectedToSend:(NSInteger)totalBytesExpected { 694 695 GDataServiceTicketBase *ticket = [fetcher propertyForKey:kFetcherTicketKey]; 696 697 [self invokeProgressCallbackForTicket:ticket 698 deliveredBytes:(unsigned long long)totalBytesSent 699 totalBytes:(unsigned long long)totalBytesExpected]; 700} 701 702- (void)objectFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error { 703 if (error) { 704 [self objectFetcher:fetcher failedWithData:data error:error]; 705 return; 706 } 707 708 // we now have the XML data for a feed or entry 709 710 // save the current thread into the fetcher, since we'll handle additional 711 // fetches and callbacks on this thread 712 [fetcher setProperty:[NSThread currentThread] 713 forKey:kFetcherCallbackThreadKey]; 714 715 // copy the run loop modes, if any, so we don't need to access them 716 // from the parsing thread 717 [fetcher setProperty:[[[self runLoopModes] copy] autorelease] 718 forKey:kFetcherCallbackRunLoopModesKey]; 719 720 // we post parsing notifications now to ensure they're on caller's 721 // original thread 722 GDataServiceTicketBase *ticket = [fetcher propertyForKey:kFetcherTicketKey]; 723 NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; 724 [defaultNC postNotificationName:kGDataServiceTicketParsingStartedNotification 725 object:ticket]; 726 727 // if there's an operation queue, then use that to schedule parsing on another 728 // thread 729 SEL parseSel = @selector(parseObjectFromDataOfFetcher:); 730 if (operationQueue_ != nil) { 731 732 NSInvocationOperation *op; 733 op = [[[NSInvocationOperation alloc] initWithTarget:self 734 selector:parseSel 735 object:fetcher] autorelease]; 736 [ticket setParseOperation:op]; 737 [operationQueue_ addOperation:op]; 738 // the fetcher now belongs to the parsing thread 739 } else { 740 // parse on the current thread, on Mac OS X 10.4 through 10.5.7 741 // or when the project defines GDATA_SKIP_PARSE_THREADING 742 [self performSelector:parseSel 743 withObject:fetcher]; 744 } 745} 746 747- (void)parseObjectFromDataOfFetcher:(GTMHTTPFetcher *)fetcher { 748 749 // this may be invoked in a separate thread 750 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; 751 752#if GDATA_LOG_PERFORMANCE 753 NSTimeInterval secs1, secs2; 754 secs1 = [NSDate timeIntervalSinceReferenceDate]; 755#endif 756 757 NSError *error = nil; 758 GDataObject* object = nil; 759 760 // Generally protect the fetcher properties, since canceling a ticket would 761 // release the fetcher properties dictionary 762 NSMutableDictionary *properties = [[[fetcher properties] retain] autorelease]; 763 764 // The callback thread is retaining the fetcher, so the fetcher shouldn't keep 765 // retaining the callback thread 766 NSThread *callbackThread = [properties valueForKey:kFetcherCallbackThreadKey]; 767 [[callbackThread retain] autorelease]; 768 [properties removeObjectForKey:kFetcherCallbackThreadKey]; 769 770 GDataServiceTicketBase *ticket = [properties valueForKey:kFetcherTicketKey]; 771 [[ticket retain] autorelease]; 772 773 NSDictionary *responseHeaders = [fetcher responseHeaders]; 774 [[responseHeaders retain] autorelease]; 775 776 NSOperation *parseOperation = [ticket parseOperation]; 777 778 Class objectClass = (Class)[properties objectForKey:kFetcherObjectClassKey]; 779 780 NSData *data = [fetcher downloadedData]; 781 NSXMLDocument *xmlDocument = [[[NSXMLDocument alloc] initWithData:data 782 options:0 783 error:&error] autorelease]; 784 if ([parseOperation isCancelled]) return; 785 786 if (xmlDocument) { 787 788 NSXMLElement* root = [xmlDocument rootElement]; 789 790 if (!objectClass) { 791 objectClass = [GDataObject objectClassForXMLElement:root]; 792 } 793 794 // see if the top-level class for the XML is listed in the surrogates; 795 // if so, instantiate the surrogate class instead 796 NSDictionary *surrogates = [ticket surrogates]; 797 Class baseSurrogate = (Class)[surrogates objectForKey:objectClass]; 798 if (baseSurrogate) { 799 objectClass = baseSurrogate; 800 } 801 802 // use the actual service version indicated by the response headers 803 NSString *serviceVersion = [responseHeaders objectForKey:@"Gdata-Version"]; 804 805 // feeds may optionally be instantiated without unknown elements tracked 806 // 807 // we only ever want to fetch feeds and discard the unknown XML, never 808 // entries 809 BOOL shouldIgnoreUnknowns = ([ticket shouldFeedsIgnoreUnknowns] 810 && [objectClass isSubclassOfClass:[GDataFeedBase class]]); 811 812 object = [[objectClass alloc] initWithXMLElement:root 813 parent:nil 814 serviceVersion:serviceVersion 815 surrogates:surrogates 816 shouldIgnoreUnknowns:shouldIgnoreUnknowns]; 817 818 // we're done parsing; the extension declarations won't be needed again 819 [object clearExtensionDeclarationsCache]; 820 821 822#if GDATA_USES_LIBXML 823 // retain the document so that pointers to internal nodes remain valid 824 [object setProperty:xmlDocument forKey:kGDataXMLDocumentPropertyKey]; 825#endif 826 827 [properties setValue:object forKey:kFetcherParsedObjectKey]; 828 [object release]; 829 830#if GDATA_LOG_PERFORMANCE 831 secs2 = [NSDate timeIntervalSinceReferenceDate]; 832 NSLog(@"allocation of %@ took %f seconds", objectClass, secs2 - secs1); 833#endif 834 } 835 [properties setValue:error forKey:kFetcherParseErrorKey]; 836 837 if ([parseOperation isCancelled]) return; 838 839 SEL parseDoneSel = @selector(handleParsedObjectForFetcher:); 840 841 if (operationQueue_ != nil) { 842 NSArray *runLoopModes = [properties valueForKey:kFetcherCallbackRunLoopModesKey]; 843 if (runLoopModes) { 844 [self performSelector:parseDoneSel 845 onThread:callbackThread 846 withObject:fetcher 847 waitUntilDone:NO 848 modes:runLoopModes]; 849 } else { 850 // defaults to common modes 851 [self performSelector:parseDoneSel 852 onThread:callbackThread 853 withObject:fetcher 854 waitUntilDone:NO]; 855 } 856 // the fetcher now belongs to the callback thread 857 } else { 858 // in 10.4, there's no performSelector:onThread: 859 [self performSelector:parseDoneSel withObject:fetcher]; 860 [properties removeObjectForKey:kFetcherCallbackThreadKey]; 861 } 862 863 // We drain here to keep the clang static analyzer quiet. 864 [pool drain]; 865} 866 867- (void)handleParsedObjectForFetcher:(GTMHTTPFetcher *)fetcher { 868 869 // after parsing is complete, this is invoked on the thread that the 870 // fetch was performed on 871 872 GDataServiceTicketBase *ticket = [fetcher propertyForKey:kFetcherTicketKey]; 873 [ticket setParseOperation:nil]; 874 875 // unpack the callback parameters 876 id delegate = [fetcher propertyForKey:kFetcherDelegateKey]; 877 GDataObject *object = [fetcher propertyForKey:kFetcherParsedObjectKey]; 878 NSError *error = [fetcher propertyForKey:kFetcherParseErrorKey]; 879 SEL finishedSelector = NSSelectorFromString([fetcher propertyForKey:kFetcherFinishedSelectorKey]); 880 881 GDataServiceCompletionHandler completionHandler; 882#if NS_BLOCKS_AVAILABLE 883 completionHandler = [fetcher propertyForKey:kFetcherCompletionHandlerKey]; 884#else 885 completionHandler = NULL; 886#endif 887 888 NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter]; 889 [defaultNC postNotificationName:kGDataServiceTicketParsingStoppedNotification 890 object:ticket]; 891 892 NSData *data = [fetcher downloadedData]; 893 NSUInteger dataLength = [data length]; 894 895 // if we created the object (or we got empty data back, as from a GData 896 // delete resource request) then we succeeded 897 if (object != nil || dataLength == 0) { 898 899 // if the user is fetching a feed and the ticket specifies that "next" links 900 // should be followed, then do that now 901 if ([ticket shouldFollowNextLinks] 902 && [object isKindOfClass:[GDataFeedBase class]]) { 903 904 GDataFeedBase *latestFeed = (GDataFeedBase *)object; 905 906 // append the latest feed 907 [ticket accumulateFeed:latestFeed]; 908 909 NSURL *nextURL = [[latestFeed nextLink] URL]; 910 if (nextURL) { 911 912 BOOL isFetchingNextFeed = [self fetchNextFeedWithURL:nextURL 913 delegate:delegate 914 didFinishedSelector:finishedSelector 915 completionHandler:completionHandler 916 ticket:ticket]; 917 918 // skip calling the callbacks since the ticket is still in progress 919 if (isFetchingNextFeed) { 920 921 return; 922 923 } else { 924 // the fetch didn't start; fall through to the callback for the 925 // feed accumulated so far 926 } 927 } 928 929 // no more "next" links are present, so we don't need to accumulate more 930 // entries 931 GDataFeedBase *accumulatedFeed = [ticket accumulatedFeed]; 932 if (accumulatedFeed) { 933 934 // remove the misleading "next" link from the accumulated feed 935 GDataLink *accumulatedFeedNextLink = [accumulatedFeed nextLink]; 936 if (accumulatedFeedNextLink) { 937 [accumulatedFeed removeLink:accumulatedFeedNextLink]; 938 } 939 940#if DEBUG && !GDATA_SKIP_NEXTLINK_WARNING 941 // each next link followed to accumulate all pages of a feed takes up to 942 // a few seconds. When multiple next links are being followed, that 943 // usually indicates that a larger page size (that is, more entries per 944 // feed fetched) should be requested. 945 // 946 // To avoid following next links, when fetching a feed, make it a query 947 // fetch instead; when fetching a query, use setMaxResults: so the feed 948 // requested is large enough to rarely need to follow next links. 949 NSUInteger feedPageCount = [ticket nextLinksFollowedCounter]; 950 if (feedPageCount > 2) { 951 NSLog(@"Fetching %@ required following %u \"next\" links; use a query with a larger setMaxResults: for faster feed accumulation", 952 NSStringFromClass([accumulatedFeed class]), 953 (unsigned int) [ticket nextLinksFollowedCounter]); 954 } 955#endif 956 // return the completed feed as the object that was fetched 957 object = accumulatedFeed; 958 } 959 } 960 961 if (finishedSelector) { 962 [[self class] invokeCallback:finishedSelector 963 target:delegate 964 ticket:ticket 965 object:object 966 error:nil]; 967 } 968 969#if NS_BLOCKS_AVAILABLE 970 if (completionHandler) { 971 completionHandler(ticket, object, nil); 972 } 973#endif 974 975 [ticket setFetchedObject:object]; 976 977 } else { 978 if (error == nil) { 979 error = [NSError errorWithDomain:kGDataServiceErrorDomain 980 code:kGDataCouldNotConstructObjectError 981 userInfo:nil]; 982 } 983 if (finishedSelector) { 984 [[self class] invokeCallback:finishedSelector 985 target:delegate 986 ticket:ticket 987 object:nil 988 error:error]; 989 } 990 991#if NS_BLOCKS_AVAILABLE 992 if (completionHandler) { 993 completionHandler(ticket, nil, error); 994 } 995#endif 996 [ticket setFetchError:error]; 997 } 998 999 [fetcher setProperties:nil]; 1000 1001 [ticket setHasCalledCallback:YES]; 1002 [ticket setCurrentFetcher:nil]; 1003} 1004 1005- (void)objectFetcher:(GTMHTTPFetcher *)fetcher failedWithData:(NSData *)data error:(NSError *)error { 1006 1007#if DEBUG 1008 NSString *dataString = [[[NSString alloc] initWithData:data 1009 encoding:NSUTF8StringEncoding] autorelease]; 1010 if (dataString) { 1011 NSLog(@"serviceBase:%@ objectFetcher:%@ failedWithStatus:%ld data:%@", 1012 self, fetcher, (long) [error code], dataString); 1013 } 1014#endif 1015 1016 id delegate = [fetcher propertyForKey:kFetcherDelegateKey]; 1017 1018 GDataServiceTicketBase *ticket = [fetcher propertyForKey:kFetcherTicketKey]; 1019 1020 NSString *finishedSelectorStr = [fetcher propertyForKey:kFetcherFinishedSelectorKey]; 1021 SEL finishedSelector = finishedSelectorStr ? NSSelectorFromString(finishedSelectorStr) : NULL; 1022 1023 if (error != nil) { 1024 // create structured errors in the userInfo, if appropriate 1025 NSDictionary *responseHeaders = [fetcher responseHeaders]; 1026 NSString *contentType = [responseHeaders objectForKey:@"Content-Type"]; 1027 1028 NSDictionary *newUserInfo = [self userInfoForErrorResponseData:data 1029 contentType:contentType 1030 previousUserInfo:[error userInfo]]; 1031 error = [NSError errorWithDomain:[error domain] 1032 code:[error code] 1033 userInfo:newUserInfo]; 1034 } 1035 1036 if (finishedSelector) { 1037 [[self class] invokeCallback:finishedSelector 1038 target:delegate 1039 ticket:ticket 1040 object:nil 1041 error:error]; 1042 } 1043 1044#if NS_BLOCKS_AVAILABLE 1045 GDataServiceCompletionHandler completionHandler; 1046 1047 completionHandler = [fetcher propertyForKey:kFetcherCompletionHandlerKey]; 1048 if (completionHandler) { 1049 completionHandler(ticket, nil, error); 1050 } 1051#endif 1052 1053 [fetcher setProperties:nil]; 1054 1055 [ticket setFetchError:error]; 1056 [ticket setHasCalledCallback:YES]; 1057 [ticket setCurrentFetcher:nil]; 1058} 1059 1060#pragma mark - 1061 1062// create an error userInfo dictionary containing a useful reason string and, 1063// for structured XML errors, a server error group object 1064- (NSDictionary *)userInfoForErrorResponseData:(NSData *)data 1065 contentType:(NSString *)contentType 1066 previousUserInfo:(NSDictionary *)previousUserInfo { 1067 // NSError's default localizedReason value looks like 1068 // "(com.google.GDataServiceDomain error -4.)" 1069 // 1070 // The NSError domain and numeric code aren't the ones we care about 1071 // so much as the error present in the server response data, so 1072 // we'll try to store a more useful reason in the userInfo dictionary 1073 1074 NSString *reasonStr = nil; 1075 NSMutableDictionary *userInfo; 1076 if (previousUserInfo) { 1077 userInfo = [NSMutableDictionary dictionaryWithDictionary:previousUserInfo]; 1078 } else { 1079 userInfo = [NSMutableDictionary dictionary]; 1080 } 1081 1082 if ([data length] > 0) { 1083 1084 // check if the response is a structured XML error string, according to the 1085 // response content-type header; if so, convert the XML to a 1086 // GDataServerErrorGroup 1087 if ([[contentType lowercaseString] hasPrefix:kXMLErrorContentType]) { 1088 1089 GDataServerErrorGroup *errorGroup 1090 = [[[GDataServerErrorGroup alloc] initWithData:data] autorelease]; 1091 1092 if (errorGroup != nil) { 1093 1094 // store the server group in the userInfo for the error 1095 [userInfo setObject:errorGroup forKey:kGDataStructuredErrorsKey]; 1096 1097 reasonStr = [[errorGroup mainError] summary]; 1098 } 1099 } 1100 1101 if ([userInfo count] == 0) { 1102 1103 // no structured XML error was available; deal with a plaintext server 1104 // error response 1105 reasonStr = [[[NSString alloc] initWithData:data 1106 encoding:NSUTF8StringEncoding] autorelease]; 1107 } 1108 } 1109 1110 if (reasonStr != nil) { 1111 // we always store an error in the userInfo key "error" 1112 [userInfo setObject:reasonStr forKey:kGDataServerErrorStringKey]; 1113 1114 // store a user-readable "reason" to show up when an error is logged, 1115 // in parentheses like NSError does it 1116 NSString *parenthesized = [NSString stringWithFormat:@"(%@)", reasonStr]; 1117 [userInfo setObject:parenthesized forKey:NSLocalizedFailureReasonErrorKey]; 1118 } 1119 1120 return userInfo; 1121} 1122 1123+ (void)invokeCallback:(SEL)callbackSel 1124 target:(id)target 1125 ticket:(id)ticket 1126 object:(id)object 1127 error:(id)error { 1128 1129 // GData fetch callbacks have no return value 1130 NSMethodSignature *signature = [target methodSignatureForSelector:callbackSel]; 1131 NSInvocation *retryInvocation = [NSInvocation invocationWithMethodSignature:signature]; 1132 [retryInvocation setSelector:callbackSel]; 1133 [retryInvocation setTarget:target]; 1134 [retryInvocation setArgument:&ticket atIndex:2]; 1135 [retryInvocation setArgument:&object atIndex:3]; 1136 [retryInvocation setArgument:&error atIndex:4]; 1137 [retryInvocation invoke]; 1138} 1139 1140// The object fetcher may call into this retry method; this one invokes the 1141// selector provided by the user. 1142- (BOOL)objectFetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)willRetry forError:(NSError *)error { 1143 1144 id delegate = [fetcher propertyForKey:kFetcherDelegateKey]; 1145 GDataServiceTicketBase *ticket = [fetcher propertyForKey:kFetcherTicketKey]; 1146 1147 SEL retrySelector = [ticket retrySelector]; 1148 if (retrySelector) { 1149 1150 willRetry = [self invokeRetrySelector:retrySelector 1151 delegate:delegate 1152 ticket:ticket 1153 willRetry:willRetry 1154 error:error]; 1155 } 1156 return willRetry; 1157} 1158 1159- (BOOL)invokeRetrySelector:(SEL)retrySelector 1160 delegate:(id)delegate 1161 ticket:(GDataServiceTicketBase *)ticket 1162 willRetry:(BOOL)willRetry 1163 error:(NSError *)error { 1164 1165 if ([delegate respondsToSelector:retrySelector]) { 1166 1167 // Unlike the retry selector invocation in GTMHTTPFetcher, this invocation 1168 // passes the ticket rather than the fetcher as argument 2 1169 NSMethodSignature *signature = [delegate methodSignatureForSelector:retrySelector]; 1170 NSInvocation *retryInvocation = [NSInvocation invocationWithMethodSignature:signature]; 1171 [retryInvocation setSelector:retrySelector]; 1172 [retryInvocation setTarget:delegate]; 1173 [retryInvocation setArgument:&ticket atIndex:2]; // ticket passed 1174 [retryInvocation setArgument:&willRetry atIndex:3]; 1175 [retryInvocation setArgument:&error atIndex:4]; 1176 [retryInvocation invoke]; 1177 1178 [retryInvocation getReturnValue:&willRetry]; 1179 } 1180 return willRetry; 1181} 1182 1183- (void)addAuthenticationToFetcher:(GTMHTTPFetcher *)fetcher { 1184 NSString *username = [self username]; 1185 NSString *password = [self password]; 1186 1187 if ([username length] > 0 && [password length] > 0) { 1188 // We're avoiding +[NSURCredential credentialWithUser:password:persistence:] 1189 // because it fails to autorelease itself on OS X 10.4 .. 10.5.x 1190 // rdar://5596278 1191 1192 NSURLCredential *cred; 1193 cred = [[[NSURLCredential alloc] initWithUser:username 1194 password:password 1195 persistence:NSURLCredentialPersistenceForSession] autorelease]; 1196 [fetcher setCredential:cred]; 1197 } 1198} 1199 1200// when a ticket is set to follow "next" links for feeds, this routine 1201// initiates the fetch for each additional feed 1202- (BOOL)fetchNextFeedWithURL:(NSURL *)nextFeedURL 1203 delegate:(id)delegate 1204 didFinishedSelector:(SEL)finishedSelector 1205 completionHandler:(GDataServiceCompletionHandler)completionHandler 1206 ticket:(GDataServiceTicketBase *)ticket { 1207 1208 // sanity check the number of pages fetched already 1209 NSUInteger followedCounter = [ticket nextLinksFollowedCounter]; 1210 1211 if (followedCounter > kMaxNumberOfNextLinksFollowed) { 1212 1213 // the client should be querying with a higher max results per page 1214 // to avoid this 1215 GDATA_DEBUG_ASSERT(0, @"Following next links retrieves too many pages (URL %@)", 1216 nextFeedURL); 1217 return NO; 1218 } 1219 1220 [ticket setNextLinksFollowedCounter:(1 + followedCounter)]; 1221 1222 // by definition, feed requests are GETs, so objectToPost: and httpMethod: 1223 // should be nil 1224 GDataServiceTicketBase *startedTicket; 1225 startedTicket = [self fetchObjectWithURL:nextFeedURL 1226 objectClass:[[ticket accumulatedFeed] class] 1227 objectToPost:nil 1228 ETag:nil 1229 httpMethod:nil 1230 delegate:delegate 1231 didFinishSelector:finishedSelector 1232 completionHandler:completionHandler 1233 retryInvocationValue:nil 1234 ticket:ticket]; 1235 1236 // in the bizarre case that the fetch didn't begin, startedTicket will be 1237 // nil. So long as the started ticket is the same as the ticket we're 1238 // continuing, then we're happy. 1239 return (ticket == startedTicket); 1240} 1241 1242 1243- (BOOL)waitForTicket:(GDataServiceTicketBase *)ticket 1244 timeout:(NSTimeInterval)timeoutInSeconds 1245 fetchedObject:(GDataObject **)outObjectOrNil 1246 error:(NSError **)outErrorOrNil { 1247 1248 NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds]; 1249 1250 // loop until the fetch completes with an object or an error, 1251 // or until the timeout has expired 1252 while (![ticket hasCalledCallback] 1253 && [giveUpDate timeIntervalSinceNow] > 0) { 1254 1255 // run the current run loop 1/1000 of a second to give the networking 1256 // code a chance to work 1257 NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001]; 1258 [[NSRunLoop currentRunLoop] runUntilDate:stopDate]; 1259 } 1260 1261 NSError *fetchError = [ticket fetchError]; 1262 1263 if (![ticket hasCalledCallback] && fetchError == nil) { 1264 fetchError = [NSError errorWithDomain:kGDataServiceErrorDomain 1265 code:kGDataWaitTimedOutError 1266 userInfo:nil]; 1267 } 1268 1269 if (outObjectOrNil) *outObjectOrNil = [ticket fetchedObject]; 1270 if (outErrorOrNil) *outErrorOrNil = fetchError; 1271 1272 return (fetchError == nil); 1273} 1274 1275#pragma mark - 1276 1277// These external entry points all call into fetchObjectWithURL: defined above 1278 1279- (GDataServiceTicketBase *)fetchPublicFeedWithURL:(NSURL *)feedURL 1280 feedClass:(Class)feedClass 1281 delegate:(id)delegate 1282 didFinishSelector:(SEL)finishedSelector { 1283 1284 return [self fetchObjectWithURL:feedURL 1285 objectClass:feedClass 1286 objectToPost:nil 1287 ETag:nil 1288 httpMethod:nil 1289 delegate:delegate 1290 didFinishSelector:finishedSelector 1291 completionHandler:nil 1292 retryInvocationValue:nil 1293 ticket:nil]; 1294} 1295 1296- (GDataServiceTicketBase *)fetchPublicEntryWithURL:(NSURL *)entryURL 1297 entryClass:(Class)entryClass 1298 delegate:(id)delegate 1299 didFinishSelector:(SEL)finishedSelector { 1300 1301 return [self fetchObjectWithURL:entryURL 1302 objectClass:entryClass 1303 objectToPost:nil 1304 ETag:nil 1305 httpMethod:nil 1306 delegate:delegate 1307 didFinishSelector:finishedSelector 1308 completionHandler:nil 1309 retryInvocationValue:nil 1310 ticket:nil]; 1311} 1312 1313- (GDataServiceTicketBase *)fetchPublicFeedWithQuery:(GDataQuery *)query 1314 feedClass:(Class)feedClass 1315 delegate:(id)delegate 1316 didFinishSelector:(SEL)finishedSelector { 1317 1318 return [self fetchPublicFeedWithURL:[query URL] 1319 feedClass:feedClass 1320 delegate:delegate 1321 didFinishSelector:finishedSelector]; 1322} 1323 1324- (GDataServiceTicketBase *)fetchPublicFeedWithBatchFeed:(GDataFeedBase *)batchFeed 1325 forFeedURL:(NSURL *)feedURL 1326 delegate:(id)delegate 1327 didFinishSelector:(SEL)finishedSelector 1328 completionHandler:(GDataServiceCompletionHandler)completionHandler { 1329 // internal routine, used for both callback and blocks style of batch feed 1330 // fetches 1331 1332 // add basic namespaces to feed, if needed 1333 if ([[batchFeed namespaces] objectForKey:kGDataNamespaceGDataPrefix] == nil) { 1334 [batchFeed addNamespaces:[GDataEntryBase baseGDataNamespaces]]; 1335 } 1336 1337 // add batch namespace, if needed 1338 if ([[batchFeed namespaces] objectForKey:kGDataNamespaceBatchPrefix] == nil) { 1339 [batchFeed addNamespaces:[GDataEntryBase batchNamespaces]]; 1340 } 1341 1342 GDataServiceTicketBase *ticket; 1343 1344 ticket = [self fetchObjectWithURL:feedURL 1345 objectClass:[batchFeed class] 1346 objectToPost:batchFeed 1347 ETag:nil 1348 httpMethod:nil 1349 delegate:delegate 1350 didFinishSelector:finishedSelector 1351 completionHandler:completionHandler 1352 retryInvocationValue:nil 1353 ticket:nil]; 1354 1355 // batch feeds never ignore unknowns, since they are intrinsically 1356 // used for updating so their entries need to include complete XML 1357 [ticket setShouldFeedsIgnoreUnknowns:NO]; 1358 1359 return ticket; 1360} 1361 1362- (GDataServiceTicketBase *)fetchPublicFeedWithBatchFeed:(GDataFeedBase *)batchFeed 1363 forFeedURL:(NSURL *)feedURL 1364 delegate:(id)delegate 1365 didFinishSelector:(SEL)finishedSelector { 1366 1367 return [self fetchPublicFeedWithBatchFeed:batchFeed 1368 forFeedURL:feedURL 1369 delegate:delegate 1370 didFinishSelector:finishedSelector 1371 completionHandler:NULL]; 1372} 1373 1374#if NS_BLOCKS_AVAILABLE 1375 1376- (GDataServiceTicketBase *)fetchPublicFeedWithURL:(NSURL *)feedURL 1377 feedClass:(Class)feedClass 1378 completionHandler:(GDataServiceFeedBaseCompletionHandler)handler { 1379 1380 return [self fetchObjectWithURL:feedURL 1381 objectClass:feedClass 1382 objectToPost:nil 1383 ETag:nil 1384 httpMethod:nil 1385 delegate:nil 1386 didFinishSelector:NULL 1387 completionHandler:(GDataServiceCompletionHandler)handler 1388 retryInvocationValue:nil 1389 ticket:nil]; 1390} 1391 1392- (GDataServiceTicketBase *)fetchPublicFeedWithQuery:(GDataQuery *)query 1393 feedClass:(Class)feedClass 1394 completionHandler:(GDataServiceFeedBaseCompletionHandler)handler { 1395return [self fetchPublicFeedWithURL:[query URL] 1396 feedClass:feedClass 1397 completionHandler:handler]; 1398} 1399 1400- (GDataServiceTicketBase *)fetchPublicEntryWithURL:(NSURL *)entryURL 1401 entryClass:(Class)entryClass 1402 completionHandler:(GDataServiceEntryBaseCompletionHandler)handler { 1403return [self fetchObjectWithURL:entryURL 1404 objectClass:entryClass 1405 objectToPost:nil 1406 ETag:nil 1407 httpMethod:nil 1408 delegate:nil 1409 didFinishSelector:NULL 1410 completionHandler:(GDataServiceCompletionHandler)handler 1411 retryInvocationValue:nil 1412 ticket:nil]; 1413} 1414 1415- (GDataServiceTicketBase *)fetchPublicFeedWithBatchFeed:(GDataFeedBase *)batchFeed 1416 forFeedURL:(NSURL *)feedURL 1417 completionHandler:(GDataServiceFeedBaseCompletionHandler)handler { 1418return [self fetchPublicFeedWithBatchFeed:batchFeed 1419 forFeedURL:feedURL 1420 delegate:nil 1421 didFinishSelector:NULL 1422 completionHandler:(GDataServiceCompletionHandler)handler]; 1423} 1424 1425#endif // NS_BLOCKS_AVAILABLE 1426 1427#pragma mark - 1428 1429// Subclasses typically implement defaultServiceVersion to specify the expected 1430// version of the feed, but clients may also explicitly set the version 1431// if they are using an instance of the base class directly. 1432+ (NSString *)defaultServiceVersion { 1433 return nil; 1434} 1435 1436- (NSString *)serviceVersion { 1437 if (serviceVersion_ != nil) { 1438 return serviceVersion_; 1439 } 1440 1441 NSString *str = [[self class] defaultServiceVersion]; 1442 return str; 1443} 1444 1445- (void)setServiceVersion:(NSString *)str { 1446 [serviceVersion_ autorelease]; 1447 serviceVersion_ = [str copy]; 1448} 1449 1450- (NSString *)userAgent { 1451 return userAgent_; 1452} 1453 1454- (void)setExactUserAgent:(NSString *)userAgent { 1455 // internal use only 1456 [userAgent_ release]; 1457 userAgent_ = [userAgent copy]; 1458} 1459 1460- (void)setUserAgent:(NSString *)userAgent { 1461 // remove whitespace and unfriendly characters 1462 NSString *str = GTMCleanedUserAgentString(userAgent); 1463 [self setExactUserAgent:str]; 1464} 1465 1466- (NSArray *)runLoopModes { 1467 return [fetcherService_ runLoopModes]; 1468} 1469 1470- (void)setRunLoopModes:(NSArray *)modes { 1471 [fetcherService_ setRunLoopModes:modes]; 1472} 1473 1474- (BOOL)shouldFetchInBackground { 1475 return [fetcherService_ shouldFetchInBackground]; 1476} 1477 1478- (void)setShouldFetchInBackground:(BOOL)flag { 1479 [fetcherService_ setShouldFetchInBackground:flag]; 1480} 1481 1482// save the username and password, converting the password to non-plaintext 1483- (void)setUserCredentialsWithUsername:(NSString *)username 1484 password:(NSString *)password { 1485 if (username && ![username_ isEqual:username]) { 1486 1487 // username changed; discard history so we're not caching for the wrong 1488 // user 1489 [fetcherService_ clearHistory]; 1490 } 1491 1492 [username_ release]; 1493 username_ = [username copy]; 1494 1495 [password_ release]; 1496 1497 if (password) { 1498 const char *utf8password = [password UTF8String]; 1499 size_t passwordLength = strlen(utf8password); 1500 password_ = [[NSMutableData alloc] initWithBytes:utf8password 1501 length:passwordLength]; 1502 XorPlainMutableData(password_); 1503 } else { 1504 password_ = nil; 1505 } 1506} 1507 1508- (NSString *)username { 1509 return username_; 1510} 1511 1512// return the password as plaintext 1513- (NSString *)password { 1514 if (password_) { 1515 XorPlainMutableData(password_); 1516 NSString *result = [[[NSString alloc] initWithData:password_ 1517 encoding:NSUTF8StringEncoding] autorelease]; 1518 XorPlainMutableData(password_); 1519 return result; 1520 } 1521 return nil; 1522} 1523 1524- (id)authorizer { 1525 return [fetcherService_ authorizer]; 1526} 1527 1528- (void)setAuthorizer:(id)obj { 1529 if (obj != nil) { 1530 // clear any existing username/password 1531 [self setUserCredentialsWithUsername:nil 1532 password:nil]; 1533 } 1534 1535 if (obj != [fetcherService_ authorizer]) { 1536 [self clearResponseDataCache]; 1537 1538 [fetcherService_ setAuthorizer:obj]; 1539 } 1540} 1541 1542// Turn on data caching to receive a copy of previously-retrieved objects. 1543// Otherwise, fetches may return status 304 (Not Modified) rather than actual 1544// data 1545- (void)setShouldCacheResponseData:(BOOL)flag { 1546 [fetcherService_ setShouldCacheETaggedData:flag]; 1547} 1548 1549- (BOOL)shouldCacheResponseData { 1550 return [fetcherService_ shouldCacheETaggedData]; 1551} 1552 1553- (void)setResponseDataCacheCapacity:(NSUInteger)totalBytes { 1554 [[fetcherService_ fetchHistory] setMemoryCapacity:totalBytes]; 1555} 1556 1557- (NSUInteger)responseDataCacheCapacity { 1558 return [[fetcherService_ fetchHistory] memoryCapacity]; 1559} 1560 1561// reset the cache to avoid getting a Not Modified status 1562// based on prior queries 1563- (void)clearResponseDataCache { 1564 [fetcherService_ clearETaggedDataCache]; 1565} 1566 1567- (void)setFetcherService:(GTMHTTPFetcherService *)obj { 1568 [fetcherService_ autorelease]; 1569 fetcherService_ = [obj retain]; 1570} 1571 1572- (GTMHTTPFetcherService *)fetcherService { 1573 return fetcherService_; 1574} 1575 1576- (void)setCookieStorageMethod:(NSInteger)method { 1577 cookieStorageMethod_ = method; 1578} 1579 1580- (NSInteger)cookieStorageMethod { 1581 return cookieStorageMethod_; 1582} 1583 1584- (void)setServiceShouldFollowNextLinks:(BOOL)flag { 1585 serviceShouldFollowNextLinks_ = flag; 1586} 1587 1588- (BOOL)serviceShouldFollowNextLinks { 1589 return serviceShouldFollowNextLinks_; 1590} 1591 1592// The service userData becomes the initial value for each future ticket's 1593// userData. 1594// 1595// Since the network transactions may begin before the client has been 1596// returned the ticket by the fetch call, it's preferable to call 1597// setServiceUserData before the ticket is created rather than call the 1598// ticket's setUserData:. Either way, the ticket's userData: 1599// method will return the value. 1600- (void)setServiceUserData:(id)userData { 1601 [serviceUserData_ autorelease]; 1602 serviceUserData_ = [userData retain]; 1603} 1604 1605- (id)serviceUserData { 1606 return serviceUserData_; 1607} 1608 1609// The service properties dictionary becomes the dictionary for each future 1610// ticket. 1611 1612- (void)setServiceProperties:(NSDictionary *)dict { 1613 [serviceProperties_ autorelease]; 1614 serviceProperties_ = [dict mutableCopy]; 1615} 1616 1617- (NSDictionary *)serviceProperties { 1618 // be sure the returned pointer has the life of the autorelease pool, 1619 // in case self is released immediately 1620 return [[serviceProperties_ retain] autorelease]; 1621} 1622 1623- (void)setServiceProperty:(id)obj forKey:(NSString *)key { 1624 1625 if (obj == nil) { 1626 // user passed in nil, so delete the property 1627 [serviceProperties_ removeObjectForKey:key]; 1628 } else { 1629 // be sure the property dictionary exists 1630 if (serviceProperties_ == nil) { 1631 [self setServiceProperties:[NSDictionary dictionary]]; 1632 } 1633 [serviceProperties_ setObject:obj forKey:key]; 1634 } 1635} 1636 1637- (id)servicePropertyForKey:(NSString *)key { 1638 id obj = [serviceProperties_ objectForKey:key]; 1639 1640 // be sure the returned pointer has the life of the autorelease pool, 1641 // in case self is released immediately 1642 return [[obj retain] autorelease]; 1643} 1644 1645- (NSDictionary *)serviceSurrogates { 1646 return serviceSurrogates_; 1647} 1648 1649- (void)setServiceSurrogates:(NSDictionary *)dict { 1650 [serviceSurrogates_ autorelease]; 1651 serviceSurrogates_ = [dict retain]; 1652} 1653 1654- (BOOL)shouldServiceFeedsIgnoreUnknowns { 1655 return shouldServiceFeedsIgnoreUnknowns_; 1656} 1657 1658- (void)setShouldServiceFeedsIgnoreUnknowns:(BOOL)flag { 1659 shouldServiceFeedsIgnoreUnknowns_ = flag; 1660} 1661 1662- (SEL)serviceUploadProgressSelector { 1663 return serviceUploadProgressSelector_; 1664} 1665 1666- (void)setServiceUploadProgressSelector:(SEL)progressSelector { 1667 serviceUploadProgressSelector_ = progressSelector; 1668} 1669 1670#if NS_BLOCKS_AVAILABLE 1671- (void)setServiceUploadProgressHandler:(GDataServiceUploadProgressHandler)block { 1672 [serviceUploadProgressBlock_ autorelease]; 1673 serviceUploadProgressBlock_ = [block copy]; 1674} 1675 1676- (GDataServiceUploadProgressHandler)serviceUploadProgressHandler { 1677 return serviceUploadProgressBlock_; 1678} 1679#endif 1680 1681+ (NSUInteger)defaultServiceUploadChunkSize { 1682 // subclasses may override 1683 return 0; 1684} 1685 1686- (NSUInteger)serviceUploadChunkSize { 1687 return uploadChunkSize_; 1688} 1689 1690- (void)setServiceUploadChunkSize:(NSUInteger)val { 1691 1692 if (val == kGDataStandardUploadChunkSize) { 1693 // determine an appropriate upload chunk size for the system 1694 1695#if GDATA_IPHONE 1696 if (![GTMHTTPFetcher doesSupportSentDataCallback]) { 1697 // for iPhone 2, we need a small upload chunk size so there 1698 // are frequent intrachunk callbacks for progress monitoring 1699 // 1700 // Picasa Web Albums has a minimum 256K chunk size when uploading photos 1701 val = 256*1024; 1702 } else { 1703 val = 1024*1024; 1704 } 1705#else 1706 if (NSFoundationVersionNumber >= 751.00) { 1707 // Mac OS X 10.6 and later 1708 // 1709 // we'll pick a huge upload chunk size, which minimizes http overhead 1710 // and server effort, and we'll hope that NSURLConnection can finally 1711 // handle big uploads reliably 1712 val = 25*1024*1024; 1713 } else { 1714 // Mac OS X 10.5 1715 // 1716 // NSURLConnection is more reliable on POSTs in 10.5 than it was in 1717 // 10.4, but it still fails mysteriously on big uploads on some 1718 // systems, so we'll limit the chunks to a megabyte 1719 val = 1024*1024; 1720 } 1721#endif 1722 } 1723 uploadChunkSize_ = val; 1724} 1725 1726// retrying; see comments on retry support at the top of GTMHTTPFetcher 1727- (BOOL)isServiceRetryEnabled { 1728 return isServiceRetryEnabled_; 1729} 1730 1731- (void)setIsServiceRetryEnabled:(BOOL)flag { 1732 isServiceRetryEnabled_ = flag; 1733} 1734 1735- (SEL)serviceRetrySelector { 1736 return serviceRetrySEL_; 1737} 1738 1739- (void)setServiceRetrySelector:(SEL)theSel { 1740 serviceRetrySEL_ = theSel; 1741} 1742 1743- (NSTimeInterval)serviceMaxRetryInterval { 1744 return serviceMaxRetryInterval_; 1745} 1746 1747- (void)setServiceMaxRetryInterval:(NSTimeInterval)secs { 1748 serviceMaxRetryInterval_ = secs; 1749} 1750 1751- (id)operationQueue { 1752 return operationQueue_; 1753} 1754 1755- (void)setOperationQueue:(id)queue { 1756 [operationQueue_ autorelease]; 1757 operationQueue_ = [queue retain]; 1758} 1759 1760#pragma mark - 1761 1762+ (NSBundle *)owningBundle { 1763 NSBundle *bundle = [NSBundle bundleForClass:self]; 1764 if (bundle == nil 1765 || [[bundle bundleIdentifier] isEqual:@"com.google.GDataFramework"]) { 1766 1767 bundle = [NSBundle mainBundle]; 1768 } 1769 return bundle; 1770} 1771 1772// return a generic name and version for the current application; this avoids 1773// anonymous server transactions. Applications should call setUserAgent 1774// to avoid the need for this method to be used. 1775+ (NSString *)defaultApplicationIdentifier { 1776 NSBundle *bundle = [[self class] owningBundle]; 1777 NSString *appID = GTMApplicationIdentifier(bundle); 1778 return appID; 1779} 1780@end 1781 1782@implementation GDataServiceTicketBase 1783 1784+ (id)ticketForService:(GDataServiceBase *)service { 1785 return [[[self alloc] initWithService:service] autorelease]; 1786} 1787 1788- (id)initWithService:(GDataServiceBase *)service { 1789 self = [super init]; 1790 if (self) { 1791 service_ = [service retain]; 1792 1793 [self setUserData:[service serviceUserData]]; 1794 [self setProperties:[service serviceProperties]]; 1795 [self setSurrogates:[service serviceSurrogates]]; 1796 [self setUploadProgressSelector:[service serviceUploadProgressSelector]]; 1797 [self setIsRetryEnabled:[service isServiceRetryEnabled]]; 1798 [self setRetrySelector:[service serviceRetrySelector]]; 1799 [self setMaxRetryInterval:[service serviceMaxRetryInterval]]; 1800 [self setShouldFollowNextLinks:[service serviceShouldFollowNextLinks]]; 1801 [self setShouldFeedsIgnoreUnknowns:[service shouldServiceFeedsIgnoreUnknowns]]; 1802#if NS_BLOCKS_AVAILABLE 1803 [self setUploadProgressHandler:[service serviceUploadProgressHandler]]; 1804#endif 1805 [self setAuthorizer:[service authorizer]]; 1806 } 1807 return self; 1808} 1809 1810- (void)dealloc { 1811 [service_ release]; 1812 1813 [userData_ release]; 1814 [ticketProperties_ release]; 1815 [surrogates_ release]; 1816 1817 [currentFetcher_ release]; 1818 [objectFetcher_ release]; 1819 1820#if NS_BLOCKS_AVAILABLE 1821 [uploadProgressBlock_ release]; 1822#endif 1823 [postedObject_ release]; 1824 [fetchedObject_ release]; 1825 [accumulatedFeed_ release]; 1826 [fetchError_ release]; 1827 1828 [parseOperation_ release]; 1829 1830 [authorizer_ release]; 1831 1832 [super dealloc]; 1833} 1834 1835- (NSString *)description { 1836 NSString *const templateStr = @"%@ %p: {service:%@ currentFetcher:%@ userData:%@}"; 1837 return [NSString stringWithFormat:templateStr, 1838 [self class], self, service_, currentFetcher_, userData_]; 1839} 1840 1841- (void)pauseUpload { 1842 BOOL canPause = [objectFetcher_ respondsToSelector:@selector(pauseFetching)]; 1843 GDATA_DEBUG_ASSERT(canPause, @"unpauseable ticket"); 1844 1845 if (canPause) { 1846 [(GTMHTTPUploadFetcher *)objectFetcher_ pauseFetching]; 1847 } 1848} 1849 1850- (void)resumeUpload { 1851 BOOL canResume = [objectFetcher_ respondsToSelector:@selector(resumeFetching)]; 1852 GDATA_DEBUG_ASSERT(canResume, @"unresumable ticket"); 1853 1854 if (canResume) { 1855 [(GTMHTTPUploadFetcher *)objectFetcher_ resumeFetching]; 1856 } 1857} 1858 1859- (BOOL)isUploadPaused { 1860 BOOL isPausable = [objectFetcher_ respondsToSelector:@selector(isPaused)]; 1861 GDATA_DEBUG_ASSERT(isPausable, @"unpauseable ticket"); 1862 1863 if (isPausable) { 1864 return [(GTMHTTPUploadFetcher *)objectFetcher_ isPaused]; 1865 } 1866 return NO; 1867} 1868 1869- (void)cancelTicket { 1870 NSOperation *op = [self parseOperation]; 1871 [op cancel]; 1872 [self setParseOperation:nil]; 1873 1874 [objectFetcher_ stopFetching]; 1875 [objectFetcher_ setProperties:nil]; 1876 1877 [self setObjectFetcher:nil]; 1878 [self setCurrentFetcher:nil]; 1879 [self setUserData:nil]; 1880 [self setProperties:nil]; 1881 1882 [self setUploadProgressSelector:NULL]; 1883#if NS_BLOCKS_AVAILABLE 1884 [self setUploadProgressHandler:nil]; 1885#endif 1886 1887 [service_ autorelease]; 1888 service_ = nil; 1889} 1890 1891- (id)service { 1892 return service_; 1893} 1894 1895- (id)userData { 1896 return [[userData_ retain] autorelease]; 1897} 1898 1899- (void)setUserData:(id)obj { 1900 [userData_ autorelease]; 1901 userData_ = [obj retain]; 1902} 1903 1904- (void)setProperties:(NSDictionary *)dict { 1905 [ticketProperties_ autorelease]; 1906 ticketProperties_ = [dict mutableCopy]; 1907} 1908 1909- (NSDictionary *)properties { 1910 // be sure the returned pointer has the life of the autorelease pool, 1911 // in case self is released immediately 1912 return [[ticketProperties_ retain] autorelease]; 1913} 1914 1915- (void)setProperty:(id)obj forKey:(NSString *)key { 1916 1917 if (obj == nil) { 1918 // user passed in nil, so delete the property 1919 [ticketProperties_ removeObjectForKey:key]; 1920 } else { 1921 // be sure the property dictionary exists 1922 if (ticketProperties_ == nil) { 1923 [self setProperties:[NSDictionary dictionary]]; 1924 } 1925 [ticketProperties_ setObject:obj forKey:key]; 1926 } 1927} 1928 1929- (id)propertyForKey:(NSString *)key { 1930 id obj = [ticketProperties_ objectForKey:key]; 1931 1932 // be sure the returned pointer has the life of the autorelease pool, 1933 // in case self is released immediately 1934 return [[obj retain] autorelease]; 1935} 1936 1937- (NSDictionary *)surrogates { 1938 return surrogates_; 1939} 1940 1941- (void)setSurrogates:(NSDictionary *)dict { 1942 [surrogates_ autorelease]; 1943 surrogates_ = [dict retain]; 1944} 1945 1946- (SEL)uploadProgressSelector { 1947 return uploadProgressSelector_; 1948} 1949 1950- (void)setUploadProgressSelector:(SEL)progressSelector { 1951 uploadProgressSelector_ = progressSelector; 1952 1953 // if the user is turning on the progress selector in the ticket after the 1954 // ticket's fetcher has been created, we need to give the fetcher our sentData 1955 // callback. 1956 if (progressSelector != NULL) { 1957 SEL sentDataSel = @selector(objectFetcher:didSendBytes:totalBytesSent:totalBytesExpectedToSend:); 1958 [[self objectFetcher] setSentDataSelector:sentDataSel]; 1959 } 1960} 1961 1962#if NS_BLOCKS_AVAILABLE 1963- (void)setUploadProgressHandler:(GDataServiceUploadProgressHandler)block { 1964 [uploadProgressBlock_ autorelease]; 1965 uploadProgressBlock_ = [block copy]; 1966 1967 if (uploadProgressBlock_) { 1968 // as above, we need the fetcher to call us back when bytes are sent 1969 SEL sentDataSel = @selector(objectFetcher:didSendBytes:totalBytesSent:totalBytesExpectedToSend:); 1970 [[self objectFetcher] setSentDataSelector:sentDataSel]; 1971 } 1972} 1973 1974- (GDataServiceUploadProgressHandler)uploadProgressHandler { 1975 return uploadProgressBlock_; 1976} 1977#endif 1978 1979- (BOOL)shouldFollowNextLinks { 1980 return shouldFollowNextLinks_; 1981} 1982 1983- (void)setShouldFollowNextLinks:(BOOL)flag { 1984 shouldFollowNextLinks_ = flag; 1985} 1986 1987- (BOOL)shouldFeedsIgnoreUnknowns { 1988 return shouldFeedsIgnoreUnknowns_; 1989} 1990 1991- (void)setShouldFeedsIgnoreUnknowns:(BOOL)flag { 1992 shouldFeedsIgnoreUnknowns_ = flag; 1993} 1994 1995- (BOOL)isRetryEnabled { 1996 return isRetryEnabled_; 1997} 1998 1999- (void)setIsRetryEnabled:(BOOL)flag { 2000 isRetryEnabled_ = flag; 2001}; 2002 2003- (SEL)retrySelector { 2004 return retrySEL_; 2005} 2006 2007- (void)setRetrySelector:(SEL)theSelector { 2008 retrySEL_ = theSelector; 2009} 2010 2011- (NSTimeInterval)maxRetryInterval { 2012 return maxRetryInterval_; 2013} 2014 2015- (void)setMaxRetryInterval:(NSTimeInterval)secs { 2016 maxRetryInterval_ = secs; 2017} 2018 2019- (GTMHTTPFetcher *)currentFetcher { 2020 return [[currentFetcher_ retain] autorelease]; 2021} 2022 2023- (void)setCurrentFetcher:(GTMHTTPFetcher *)fetcher { 2024 [currentFetcher_ autorelease]; 2025 currentFetcher_ = [fetcher retain]; 2026} 2027 2028- (GTMHTTPFetcher *)objectFetcher { 2029 return [[objectFetcher_ retain] autorelease]; 2030} 2031 2032- (void)setObjectFetcher:(GTMHTTPFetcher *)fetcher { 2033 [objectFetcher_ autorelease]; 2034 objectFetcher_ = [fetcher retain]; 2035} 2036 2037- (NSInteger)statusCode { 2038 return [objectFetcher_ statusCode]; 2039} 2040 2041- (void)setHasCalledCallback:(BOOL)flag { 2042 hasCalledCallback_ = flag; 2043} 2044 2045- (BOOL)hasCalledCallback { 2046 return hasCalledCallback_; 2047} 2048 2049- (void)setPostedObject:(GDataObject *)obj { 2050 [postedObject_ autorelease]; 2051 postedObject_ = [obj retain]; 2052} 2053 2054- (id)postedObject { 2055 return postedObject_; 2056} 2057 2058- (void)setFetchedObject:(GDataObject *)obj { 2059 [fetchedObject_ autorelease]; 2060 fetchedObject_ = [obj retain]; 2061} 2062 2063- (GDataObject *)fetchedObject { 2064 return fetchedObject_; 2065} 2066 2067- (void)setFetchError:(NSError *)error { 2068 [fetchError_ autorelease]; 2069 fetchError_ = [error retain]; 2070} 2071 2072- (NSError *)fetchError { 2073 return fetchError_; 2074} 2075 2076- (void)setAccumulatedFeed:(GDataFeedBase *)feed { 2077 [accumulatedFeed_ autorelease]; 2078 accumulatedFeed_ = [feed retain]; 2079} 2080 2081// accumulateFeed is used by the service to append an incomplete feed 2082// to the ticket when shouldFollowNextLinks is enabled 2083- (GDataFeedBase *)accumulatedFeed { 2084 return accumulatedFeed_; 2085} 2086 2087- (void)accumulateFeed:(GDataFeedBase *)newFeed { 2088 2089 GDataFeedBase *accumulatedFeed = [self accumulatedFeed]; 2090 if (accumulatedFeed == nil) { 2091 2092 // the new feed becomes the accumulated feed 2093 [self setAccumulatedFeed:newFeed]; 2094 2095 } else { 2096 2097 // A feed's addEntry: routine requires that a new entry's parent 2098 // not be set to some other feed. Calling addEntryWithEntry: would make 2099 // a new, parentless copy of the entry for us, but that would be a needless 2100 // copy. Instead, we'll explicitly clear the entry's parent and call 2101 // addEntry:. 2102 2103 NSArray *newEntries = [newFeed entries]; 2104 2105 for (GDataEntryBase *entry in newEntries) { 2106 [entry setParent:nil]; 2107 [accumulatedFeed addEntry:entry]; 2108 } 2109 } 2110} 2111 2112- (void)setNextLinksFollowedCounter:(NSUInteger)val { 2113 nextLinksFollowedCounter_ = val; 2114} 2115 2116- (NSUInteger)nextLinksFollowedCounter { 2117 return nextLinksFollowedCounter_; 2118} 2119 2120- (NSOperation *)parseOperation { 2121 return parseOperation_; 2122} 2123 2124- (void)setParseOperation:(NSOperation *)op { 2125 [parseOperation_ autorelease]; 2126 parseOperation_ = [op retain]; 2127} 2128 2129// OAuth support 2130- (id)authorizer { 2131 return authorizer_; 2132} 2133 2134- (void)setAuthorizer:(id)obj { 2135 [authorizer_ autorelease]; 2136 authorizer_ = [obj retain]; 2137 2138 GDATA_DEBUG_ASSERT([obj respondsToSelector:@selector(authorizeRequest:delegate:didFinishSelector:)] 2139 || obj == nil, @"invalid authorization object"); 2140} 2141 2142@end