PageRenderTime 368ms CodeModel.GetById 19ms app.highlight 333ms RepoModel.GetById 1ms app.codeStats 0ms

/core/externals/update-engine/externals/gdata-objectivec-client/Source/BaseClasses/GDataServiceBase.m

http://macfuse.googlecode.com/
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