2388 lines
90 KiB
Mathematica
2388 lines
90 KiB
Mathematica
|
/* Copyright (c) 2011 Google Inc.
|
||
|
*
|
||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
* you may not use this file except in compliance with the License.
|
||
|
* You may obtain a copy of the License at
|
||
|
*
|
||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
*
|
||
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
* See the License for the specific language governing permissions and
|
||
|
* limitations under the License.
|
||
|
*/
|
||
|
|
||
|
//
|
||
|
// GTLService.m
|
||
|
//
|
||
|
|
||
|
#import <TargetConditionals.h>
|
||
|
#if TARGET_OS_MAC
|
||
|
#include <sys/utsname.h>
|
||
|
#endif
|
||
|
|
||
|
#if TARGET_OS_IPHONE
|
||
|
#import <UIKit/UIKit.h>
|
||
|
#endif
|
||
|
|
||
|
#define GTLSERVICE_DEFINE_GLOBALS 1
|
||
|
#import "GTLService.h"
|
||
|
|
||
|
static NSString *const kUserDataPropertyKey = @"_userData";
|
||
|
|
||
|
static NSString* const kFetcherDelegateKey = @"_delegate";
|
||
|
static NSString* const kFetcherObjectClassKey = @"_objectClass";
|
||
|
static NSString* const kFetcherFinishedSelectorKey = @"_finishedSelector";
|
||
|
static NSString* const kFetcherCompletionHandlerKey = @"_completionHandler";
|
||
|
static NSString* const kFetcherTicketKey = @"_ticket";
|
||
|
static NSString* const kFetcherFetchErrorKey = @"_fetchError";
|
||
|
static NSString* const kFetcherParsingNotificationKey = @"_parseNotification";
|
||
|
static NSString* const kFetcherParsedObjectKey = @"_parsedObject";
|
||
|
static NSString* const kFetcherBatchClassMapKey = @"_batchClassMap";
|
||
|
static NSString* const kFetcherCallbackThreadKey = @"_callbackThread";
|
||
|
static NSString* const kFetcherCallbackRunLoopModesKey = @"_runLoopModes";
|
||
|
|
||
|
static const NSUInteger kMaxNumberOfNextPagesFetched = 25;
|
||
|
|
||
|
// we'll enforce 50K chunks minimum just to avoid the server getting hit
|
||
|
// with too many small upload chunks
|
||
|
static const NSUInteger kMinimumUploadChunkSize = 50000;
|
||
|
static const NSUInteger kStandardUploadChunkSize = NSUIntegerMax;
|
||
|
|
||
|
// Helper to get the ETag if it is defined on an object.
|
||
|
static NSString *ETagIfPresent(GTLObject *obj) {
|
||
|
NSString *result = [obj.JSON objectForKey:@"etag"];
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
@interface GTLServiceTicket ()
|
||
|
@property (retain) NSOperation *parseOperation;
|
||
|
@property (assign) BOOL isREST;
|
||
|
@end
|
||
|
|
||
|
// category to provide opaque access to tickets stored in fetcher properties
|
||
|
@implementation GTMHTTPFetcher (GTLServiceTicketAdditions)
|
||
|
- (id)ticket {
|
||
|
return [self propertyForKey:kFetcherTicketKey];
|
||
|
}
|
||
|
@end
|
||
|
|
||
|
// If GTMHTTPUploadFetcher is available, it can be used for chunked uploads
|
||
|
//
|
||
|
// We locally declare some methods of GTMHTTPUploadFetcher so we
|
||
|
// do not need to import the header, as some projects may not have it available
|
||
|
@interface GTMHTTPUploadFetcher : GTMHTTPFetcher
|
||
|
+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
|
||
|
uploadData:(NSData *)data
|
||
|
uploadMIMEType:(NSString *)uploadMIMEType
|
||
|
chunkSize:(NSUInteger)chunkSize
|
||
|
fetcherService:(GTMHTTPFetcherService *)fetcherService;
|
||
|
+ (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
|
||
|
uploadFileHandle:(NSFileHandle *)uploadFileHandle
|
||
|
uploadMIMEType:(NSString *)uploadMIMEType
|
||
|
chunkSize:(NSUInteger)chunkSize
|
||
|
fetcherService:(GTMHTTPFetcherService *)fetcherService;
|
||
|
+ (GTMHTTPUploadFetcher *)uploadFetcherWithLocation:(NSURL *)location
|
||
|
uploadFileHandle:(NSFileHandle *)fileHandle
|
||
|
uploadMIMEType:(NSString *)uploadMIMEType
|
||
|
chunkSize:(NSUInteger)chunkSize
|
||
|
fetcherService:(GTMHTTPFetcherService *)fetcherService;
|
||
|
- (void)pauseFetching;
|
||
|
- (void)resumeFetching;
|
||
|
- (BOOL)isPaused;
|
||
|
@end
|
||
|
|
||
|
|
||
|
@interface GTLService ()
|
||
|
- (void)prepareToParseObjectForFetcher:(GTMHTTPFetcher *)fetcher;
|
||
|
- (void)handleParsedObjectForFetcher:(GTMHTTPFetcher *)fetcher;
|
||
|
- (BOOL)fetchNextPageWithQuery:(GTLQuery *)query
|
||
|
delegate:(id)delegate
|
||
|
didFinishedSelector:(SEL)finishedSelector
|
||
|
completionHandler:(GTLServiceCompletionHandler)completionHandler
|
||
|
ticket:(GTLServiceTicket *)ticket;
|
||
|
- (id <GTLQueryProtocol>)nextPageQueryForQuery:(GTLQuery *)query
|
||
|
result:(GTLObject *)object
|
||
|
ticket:(GTLServiceTicket *)ticket;
|
||
|
- (GTLObject *)mergedNewResultObject:(GTLObject *)newResult
|
||
|
oldResultObject:(GTLObject *)oldResult
|
||
|
forQuery:(GTLQuery *)query;
|
||
|
- (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
|
||
|
fetcherService:(GTMHTTPFetcherService *)fetcherService
|
||
|
params:(GTLUploadParameters *)uploadParams;
|
||
|
+ (void)invokeCallback:(SEL)callbackSel
|
||
|
target:(id)target
|
||
|
ticket:(id)ticket
|
||
|
object:(id)object
|
||
|
error:(id)error;
|
||
|
- (BOOL)invokeRetrySelector:(SEL)retrySelector
|
||
|
delegate:(id)delegate
|
||
|
ticket:(GTLServiceTicket *)ticket
|
||
|
willRetry:(BOOL)willRetry
|
||
|
error:(NSError *)error;
|
||
|
- (BOOL)objectFetcher:(GTMHTTPFetcher *)fetcher
|
||
|
willRetry:(BOOL)willRetry
|
||
|
forError:(NSError *)error;
|
||
|
- (void)objectFetcher:(GTMHTTPFetcher *)fetcher
|
||
|
finishedWithData:(NSData *)data
|
||
|
error:(NSError *)error;
|
||
|
- (void)parseObjectFromDataOfFetcher:(GTMHTTPFetcher *)fetcher;
|
||
|
@end
|
||
|
|
||
|
@interface GTLObject (StandardProperties)
|
||
|
@property (retain) NSString *ETag;
|
||
|
@property (retain) NSString *nextPageToken;
|
||
|
@property (retain) NSNumber *nextStartIndex;
|
||
|
@end
|
||
|
|
||
|
@implementation GTLService
|
||
|
|
||
|
@synthesize userAgentAddition = userAgentAddition_,
|
||
|
fetcherService = fetcherService_,
|
||
|
parseQueue = parseQueue_,
|
||
|
shouldFetchNextPages = shouldFetchNextPages_,
|
||
|
surrogates = surrogates_,
|
||
|
uploadProgressSelector = uploadProgressSelector_,
|
||
|
retryEnabled = isRetryEnabled_,
|
||
|
retrySelector = retrySelector_,
|
||
|
maxRetryInterval = maxRetryInterval_,
|
||
|
APIKey = apiKey_,
|
||
|
isRESTDataWrapperRequired = isRESTDataWrapperRequired_,
|
||
|
urlQueryParameters = urlQueryParameters_,
|
||
|
additionalHTTPHeaders = additionalHTTPHeaders_,
|
||
|
apiVersion = apiVersion_,
|
||
|
rpcURL = rpcURL_,
|
||
|
rpcUploadURL = rpcUploadURL_;
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
@synthesize retryBlock = retryBlock_,
|
||
|
uploadProgressBlock = uploadProgressBlock_;
|
||
|
#endif
|
||
|
|
||
|
+ (Class)ticketClass {
|
||
|
return [GTLServiceTicket class];
|
||
|
}
|
||
|
|
||
|
- (id)init {
|
||
|
self = [super init];
|
||
|
if (self) {
|
||
|
|
||
|
#if GTL_IPHONE || (MAC_OS_X_VERSION_MIN_REQUIRED > MAC_OS_X_VERSION_10_5)
|
||
|
// For 10.6 and up, always use an operation queue
|
||
|
parseQueue_ = [[NSOperationQueue alloc] init];
|
||
|
#elif !GTL_SKIP_PARSE_THREADING
|
||
|
// Avoid NSOperationQueue prior to 10.5.6, per
|
||
|
// http://www.mikeash.com/?page=pyblog/use-nsoperationqueue.html
|
||
|
SInt32 bcdSystemVersion = 0;
|
||
|
(void) Gestalt(gestaltSystemVersion, &bcdSystemVersion);
|
||
|
|
||
|
if (bcdSystemVersion >= 0x1057) {
|
||
|
parseQueue_ = [[NSOperationQueue alloc] init];
|
||
|
}
|
||
|
#else
|
||
|
// parseQueue_ defaults to nil, so parsing will be done immediately
|
||
|
// on the current thread
|
||
|
#endif
|
||
|
|
||
|
fetcherService_ = [[GTMHTTPFetcherService alloc] init];
|
||
|
|
||
|
NSUInteger chunkSize = [[self class] defaultServiceUploadChunkSize];
|
||
|
self.serviceUploadChunkSize = chunkSize;
|
||
|
}
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
- (void)dealloc {
|
||
|
[parseQueue_ release];
|
||
|
[userAgent_ release];
|
||
|
[fetcherService_ release];
|
||
|
[userAgentAddition_ release];
|
||
|
[serviceProperties_ release];
|
||
|
[surrogates_ release];
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
[uploadProgressBlock_ release];
|
||
|
[retryBlock_ release];
|
||
|
#endif
|
||
|
[apiKey_ release];
|
||
|
[apiVersion_ release];
|
||
|
[rpcURL_ release];
|
||
|
[rpcUploadURL_ release];
|
||
|
[urlQueryParameters_ release];
|
||
|
[additionalHTTPHeaders_ release];
|
||
|
|
||
|
[super dealloc];
|
||
|
}
|
||
|
|
||
|
- (NSString *)requestUserAgent {
|
||
|
NSString *userAgent = self.userAgent;
|
||
|
if ([userAgent length] == 0) {
|
||
|
// the service instance is missing an explicit user-agent; use the bundle ID
|
||
|
// or process name
|
||
|
NSBundle *owningBundle = [NSBundle bundleForClass:[self class]];
|
||
|
if (owningBundle == nil
|
||
|
|| [[owningBundle bundleIdentifier] isEqual:@"com.google.GTLFramework"]) {
|
||
|
|
||
|
owningBundle = [NSBundle mainBundle];
|
||
|
}
|
||
|
|
||
|
userAgent = GTMApplicationIdentifier(owningBundle);
|
||
|
}
|
||
|
|
||
|
NSString *requestUserAgent = userAgent;
|
||
|
|
||
|
// if the user agent already specifies the library version, we'll
|
||
|
// use it verbatim in the request
|
||
|
NSString *libraryString = @"google-api-objc-client";
|
||
|
NSRange libRange = [userAgent rangeOfString:libraryString
|
||
|
options:NSCaseInsensitiveSearch];
|
||
|
if (libRange.location == NSNotFound) {
|
||
|
// the user agent doesn't specify the client library, so append that
|
||
|
// information, and the system version
|
||
|
NSString *libVersionString = GTLFrameworkVersionString();
|
||
|
|
||
|
NSString *systemString = GTMSystemVersionString();
|
||
|
|
||
|
// We don't clean this with GTMCleanedUserAgentString so spaces are
|
||
|
// preserved
|
||
|
NSString *userAgentAddition = self.userAgentAddition;
|
||
|
NSString *customString = userAgentAddition ?
|
||
|
[@" " stringByAppendingString:userAgentAddition] : @"";
|
||
|
|
||
|
// Google servers look for gzip in the user agent before sending gzip-
|
||
|
// encoded responses. See Service.java
|
||
|
requestUserAgent = [NSString stringWithFormat:@"%@ %@/%@ %@%@ (gzip)",
|
||
|
userAgent, libraryString, libVersionString, systemString, customString];
|
||
|
}
|
||
|
return requestUserAgent;
|
||
|
}
|
||
|
|
||
|
- (NSMutableURLRequest *)requestForURL:(NSURL *)url
|
||
|
ETag:(NSString *)etag
|
||
|
httpMethod:(NSString *)httpMethod
|
||
|
ticket:(GTLServiceTicket *)ticket {
|
||
|
|
||
|
// subclasses may add headers to this
|
||
|
NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] initWithURL:url
|
||
|
cachePolicy:NSURLRequestReloadIgnoringCacheData
|
||
|
timeoutInterval:60] autorelease];
|
||
|
NSString *requestUserAgent = self.requestUserAgent;
|
||
|
[request setValue:requestUserAgent forHTTPHeaderField:@"User-Agent"];
|
||
|
|
||
|
if ([httpMethod length] > 0) {
|
||
|
[request setHTTPMethod:httpMethod];
|
||
|
}
|
||
|
|
||
|
if ([etag length] > 0) {
|
||
|
|
||
|
// it's rather unexpected for an etagged object to be provided for a GET,
|
||
|
// but we'll check for an etag anyway, similar to HttpGDataRequest.java,
|
||
|
// and if present use it to request only an unchanged resource
|
||
|
|
||
|
BOOL isDoingHTTPGet = (httpMethod == nil
|
||
|
|| [httpMethod caseInsensitiveCompare:@"GET"] == NSOrderedSame);
|
||
|
|
||
|
if (isDoingHTTPGet) {
|
||
|
|
||
|
// set the etag header, even if weak, indicating we don't want
|
||
|
// another copy of the resource if it's the same as the object
|
||
|
[request setValue:etag forHTTPHeaderField:@"If-None-Match"];
|
||
|
|
||
|
} else {
|
||
|
|
||
|
// if we're doing PUT or DELETE, set the etag header indicating
|
||
|
// we only want to update the resource if our copy matches the current
|
||
|
// one (unless the etag is weak and so shouldn't be a constraint at all)
|
||
|
BOOL isWeakETag = [etag hasPrefix:@"W/"];
|
||
|
|
||
|
BOOL isModifying =
|
||
|
[httpMethod caseInsensitiveCompare:@"PUT"] == NSOrderedSame
|
||
|
|| [httpMethod caseInsensitiveCompare:@"DELETE"] == NSOrderedSame
|
||
|
|| [httpMethod caseInsensitiveCompare:@"PATCH"] == NSOrderedSame;
|
||
|
|
||
|
if (isModifying && !isWeakETag) {
|
||
|
[request setValue:etag forHTTPHeaderField:@"If-Match"];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return request;
|
||
|
}
|
||
|
|
||
|
- (NSMutableURLRequest *)requestForURL:(NSURL *)url
|
||
|
ETag:(NSString *)etag
|
||
|
httpMethod:(NSString *)httpMethod {
|
||
|
// this public entry point authenticates from the service object but
|
||
|
// not from the auth token in the ticket
|
||
|
return [self requestForURL:url ETag:etag httpMethod:httpMethod ticket:nil];
|
||
|
}
|
||
|
|
||
|
// objectRequestForURL returns an NSMutableURLRequest for a GTLObject
|
||
|
//
|
||
|
// the object is the object being sent to the server, or nil;
|
||
|
// the http method may be nil for get, or POST, PUT, DELETE
|
||
|
|
||
|
- (NSMutableURLRequest *)objectRequestForURL:(NSURL *)url
|
||
|
object:(GTLObject *)object
|
||
|
ETag:(NSString *)etag
|
||
|
httpMethod:(NSString *)httpMethod
|
||
|
isREST:(BOOL)isREST
|
||
|
additionalHeaders:(NSDictionary *)additionalHeaders
|
||
|
ticket:(GTLServiceTicket *)ticket {
|
||
|
if (object) {
|
||
|
// if the object being sent has an etag, add it to the request header to
|
||
|
// avoid retrieving a duplicate or to avoid writing over an updated
|
||
|
// version of the resource on the server
|
||
|
//
|
||
|
// Typically, delete requests will provide an explicit ETag parameter, and
|
||
|
// other requests will have the ETag carried inside the object being updated
|
||
|
if (etag == nil) {
|
||
|
SEL selEtag = @selector(ETag);
|
||
|
if ([object respondsToSelector:selEtag]) {
|
||
|
etag = [object performSelector:selEtag];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
NSMutableURLRequest *request = [self requestForURL:url
|
||
|
ETag:etag
|
||
|
httpMethod:httpMethod
|
||
|
ticket:ticket];
|
||
|
NSString *acceptValue;
|
||
|
NSString *contentTypeValue;
|
||
|
if (isREST) {
|
||
|
acceptValue = @"application/json";
|
||
|
contentTypeValue = @"application/json; charset=utf-8";
|
||
|
} else {
|
||
|
acceptValue = @"application/json-rpc";
|
||
|
contentTypeValue = @"application/json-rpc; charset=utf-8";
|
||
|
}
|
||
|
[request setValue:acceptValue forHTTPHeaderField:@"Accept"];
|
||
|
[request setValue:contentTypeValue forHTTPHeaderField:@"Content-Type"];
|
||
|
|
||
|
[request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
|
||
|
|
||
|
// Add the additional http headers from the service, and then from the query
|
||
|
NSDictionary *headers = self.additionalHTTPHeaders;
|
||
|
for (NSString *key in headers) {
|
||
|
NSString *value = [headers valueForKey:key];
|
||
|
[request setValue:value forHTTPHeaderField:key];
|
||
|
}
|
||
|
|
||
|
headers = additionalHeaders;
|
||
|
for (NSString *key in headers) {
|
||
|
NSString *value = [headers valueForKey:key];
|
||
|
[request setValue:value forHTTPHeaderField:key];
|
||
|
}
|
||
|
|
||
|
return request;
|
||
|
}
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
// common fetch starting method
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL
|
||
|
objectClass:(Class)objectClass
|
||
|
bodyObject:(GTLObject *)bodyObject
|
||
|
dataToPost:(NSData *)dataToPost
|
||
|
ETag:(NSString *)etag
|
||
|
httpMethod:(NSString *)httpMethod
|
||
|
mayAuthorize:(BOOL)mayAuthorize
|
||
|
isREST:(BOOL)isREST
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector
|
||
|
completionHandler:(id)completionHandler // GTLServiceCompletionHandler
|
||
|
executingQuery:(id<GTLQueryProtocol>)query
|
||
|
ticket:(GTLServiceTicket *)ticket {
|
||
|
|
||
|
GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector, @encode(GTLServiceTicket *), @encode(GTLObject *), @encode(NSError *), 0);
|
||
|
|
||
|
// The completionHandler argument is declared as an id, not as a block
|
||
|
// pointer, so this can be built with the 10.6 SDK and still run on 10.5.
|
||
|
// If the argument were declared as a block pointer, the invocation for
|
||
|
// fetchObjectWithURL: created in GTLService would cause an exception
|
||
|
// since 10.5's NSInvocation cannot deal with encoding of block pointers.
|
||
|
|
||
|
GTL_DEBUG_ASSERT(targetURL != nil, @"no url?");
|
||
|
if (targetURL == nil) return nil;
|
||
|
|
||
|
// we need to create a ticket unless one was created earlier (like during
|
||
|
// authentication)
|
||
|
if (!ticket) {
|
||
|
ticket = [[[self class] ticketClass] ticketForService:self];
|
||
|
}
|
||
|
|
||
|
ticket.isREST = isREST;
|
||
|
|
||
|
// Add any service specific query parameters.
|
||
|
NSDictionary *urlQueryParameters = self.urlQueryParameters;
|
||
|
if ([urlQueryParameters count] > 0) {
|
||
|
targetURL = [GTLUtilities URLWithString:[targetURL absoluteString]
|
||
|
queryParameters:urlQueryParameters];
|
||
|
}
|
||
|
|
||
|
// If this is REST and there is a developer key, add it onto the url. RPC
|
||
|
// adds the key into the payload, not on the url.
|
||
|
NSString *apiKey = self.APIKey;
|
||
|
if (isREST && [apiKey length] > 0) {
|
||
|
NSString *const kDeveloperAPIQueryParamKey = @"key";
|
||
|
NSDictionary *queryParameters;
|
||
|
queryParameters = [NSDictionary dictionaryWithObject:apiKey
|
||
|
forKey:kDeveloperAPIQueryParamKey];
|
||
|
targetURL = [GTLUtilities URLWithString:[targetURL absoluteString]
|
||
|
queryParameters:queryParameters];
|
||
|
}
|
||
|
|
||
|
NSDictionary *additionalHeaders = query.additionalHTTPHeaders;
|
||
|
|
||
|
NSMutableURLRequest *request = [self objectRequestForURL:targetURL
|
||
|
object:bodyObject
|
||
|
ETag:etag
|
||
|
httpMethod:httpMethod
|
||
|
isREST:isREST
|
||
|
additionalHeaders:additionalHeaders
|
||
|
ticket:ticket];
|
||
|
|
||
|
GTMAssertSelectorNilOrImplementedWithArgs(delegate, ticket.uploadProgressSelector,
|
||
|
@encode(GTLServiceTicket *), @encode(unsigned long long),
|
||
|
@encode(unsigned long long), 0);
|
||
|
GTMAssertSelectorNilOrImplementedWithArgs(delegate, ticket.retrySelector,
|
||
|
@encode(GTLServiceTicket *), @encode(BOOL), @encode(NSError *), 0);
|
||
|
|
||
|
SEL finishedSel = @selector(objectFetcher:finishedWithData:error:);
|
||
|
|
||
|
ticket.postedObject = bodyObject;
|
||
|
|
||
|
ticket.executingQuery = query;
|
||
|
if (ticket.originalQuery == nil) {
|
||
|
ticket.originalQuery = query;
|
||
|
}
|
||
|
|
||
|
GTMHTTPFetcherService *fetcherService = self.fetcherService;
|
||
|
GTMHTTPFetcher *fetcher;
|
||
|
|
||
|
GTLUploadParameters *uploadParams = query.uploadParameters;
|
||
|
if (uploadParams == nil) {
|
||
|
// Not uploading a file with this request
|
||
|
fetcher = [fetcherService fetcherWithRequest:request];
|
||
|
} else {
|
||
|
fetcher = [self uploadFetcherWithRequest:request
|
||
|
fetcherService:fetcherService
|
||
|
params:uploadParams];
|
||
|
}
|
||
|
|
||
|
if (finishedSelector) {
|
||
|
// if we don't have a method name, default to the finished selector as
|
||
|
// a useful fetcher log comment
|
||
|
fetcher.comment = NSStringFromSelector(finishedSelector);
|
||
|
}
|
||
|
|
||
|
// allow the user to specify static app-wide cookies for fetching
|
||
|
NSInteger cookieStorageMethod = [self cookieStorageMethod];
|
||
|
if (cookieStorageMethod >= 0) {
|
||
|
fetcher.cookieStorageMethod = cookieStorageMethod;
|
||
|
}
|
||
|
|
||
|
if (!mayAuthorize) {
|
||
|
fetcher.authorizer = nil;
|
||
|
}
|
||
|
|
||
|
// copy the ticket's retry settings into the fetcher
|
||
|
fetcher.retryEnabled = ticket.retryEnabled;
|
||
|
fetcher.maxRetryInterval = ticket.maxRetryInterval;
|
||
|
|
||
|
BOOL shouldExamineRetries;
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
shouldExamineRetries = (ticket.retrySelector != nil
|
||
|
|| ticket.retryBlock != nil);
|
||
|
#else
|
||
|
shouldExamineRetries = (ticket.retrySelector != nil);
|
||
|
#endif
|
||
|
if (shouldExamineRetries) {
|
||
|
[fetcher setRetrySelector:@selector(objectFetcher:willRetry:forError:)];
|
||
|
}
|
||
|
|
||
|
// remember the object fetcher in the ticket
|
||
|
ticket.objectFetcher = fetcher;
|
||
|
|
||
|
// add parameters used by the callbacks
|
||
|
|
||
|
[fetcher setProperty:objectClass forKey:kFetcherObjectClassKey];
|
||
|
|
||
|
[fetcher setProperty:delegate forKey:kFetcherDelegateKey];
|
||
|
|
||
|
[fetcher setProperty:NSStringFromSelector(finishedSelector)
|
||
|
forKey:kFetcherFinishedSelectorKey];
|
||
|
|
||
|
[fetcher setProperty:ticket
|
||
|
forKey:kFetcherTicketKey];
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
// copy the completion handler block to the heap; this does nothing if the
|
||
|
// block is already on the heap
|
||
|
completionHandler = [[completionHandler copy] autorelease];
|
||
|
[fetcher setProperty:completionHandler
|
||
|
forKey:kFetcherCompletionHandlerKey];
|
||
|
#endif
|
||
|
|
||
|
// set the upload data
|
||
|
fetcher.postData = dataToPost;
|
||
|
|
||
|
// failed fetches call the failure selector, which will delete the ticket
|
||
|
BOOL didFetch = [fetcher beginFetchWithDelegate:self
|
||
|
didFinishSelector:finishedSel];
|
||
|
|
||
|
// If something weird happens and the networking callbacks have been called
|
||
|
// already synchronously, we don't want to return the ticket since the caller
|
||
|
// will never know when to stop retaining it, so we'll make sure the
|
||
|
// success/failure callbacks have not yet been called by checking the
|
||
|
// ticket
|
||
|
if (!didFetch || ticket.hasCalledCallback) {
|
||
|
fetcher.properties = nil;
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
return ticket;
|
||
|
}
|
||
|
|
||
|
- (GTMHTTPUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request
|
||
|
fetcherService:(GTMHTTPFetcherService *)fetcherService
|
||
|
params:(GTLUploadParameters *)uploadParams {
|
||
|
// Hang on to the user's requested chunk size, and ensure it's not tiny
|
||
|
NSUInteger uploadChunkSize = [self serviceUploadChunkSize];
|
||
|
if (uploadChunkSize < kMinimumUploadChunkSize) {
|
||
|
uploadChunkSize = kMinimumUploadChunkSize;
|
||
|
}
|
||
|
|
||
|
#ifdef GTL_TARGET_NAMESPACE
|
||
|
// Prepend the class name prefix
|
||
|
Class uploadClass = NSClassFromString(@GTL_TARGET_NAMESPACE_STRING
|
||
|
"_GTMHTTPUploadFetcher");
|
||
|
#else
|
||
|
Class uploadClass = NSClassFromString(@"GTMHTTPUploadFetcher");
|
||
|
#endif
|
||
|
GTL_ASSERT(uploadClass != nil, @"GTMHTTPUploadFetcher needed");
|
||
|
|
||
|
NSString *uploadMIMEType = uploadParams.MIMEType;
|
||
|
NSData *uploadData = uploadParams.data;
|
||
|
NSFileHandle *uploadFileHandle = uploadParams.fileHandle;
|
||
|
NSURL *uploadLocationURL = uploadParams.uploadLocationURL;
|
||
|
|
||
|
GTMHTTPUploadFetcher *fetcher;
|
||
|
if (uploadData) {
|
||
|
fetcher = [uploadClass uploadFetcherWithRequest:request
|
||
|
uploadData:uploadData
|
||
|
uploadMIMEType:uploadMIMEType
|
||
|
chunkSize:uploadChunkSize
|
||
|
fetcherService:fetcherService];
|
||
|
} else if (uploadLocationURL) {
|
||
|
GTL_DEBUG_ASSERT(uploadFileHandle != nil,
|
||
|
@"Resume requires a file handle");
|
||
|
fetcher = [uploadClass uploadFetcherWithLocation:uploadLocationURL
|
||
|
uploadFileHandle:uploadFileHandle
|
||
|
uploadMIMEType:uploadMIMEType
|
||
|
chunkSize:uploadChunkSize
|
||
|
fetcherService:fetcherService];
|
||
|
} else {
|
||
|
fetcher = [uploadClass uploadFetcherWithRequest:request
|
||
|
uploadFileHandle:uploadFileHandle
|
||
|
uploadMIMEType:uploadMIMEType
|
||
|
chunkSize:uploadChunkSize
|
||
|
fetcherService:fetcherService];
|
||
|
}
|
||
|
|
||
|
NSString *slug = [uploadParams slug];
|
||
|
if ([slug length] > 0) {
|
||
|
[[fetcher mutableRequest] setValue:slug forHTTPHeaderField:@"Slug"];
|
||
|
}
|
||
|
return fetcher;
|
||
|
}
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
// RPC fetch methods
|
||
|
|
||
|
- (NSDictionary *)rpcPayloadForMethodNamed:(NSString *)methodName
|
||
|
parameters:(NSDictionary *)parameters
|
||
|
bodyObject:(GTLObject *)bodyObject
|
||
|
requestID:(NSString *)requestID {
|
||
|
GTL_DEBUG_ASSERT([requestID length] > 0, @"Got an empty request id");
|
||
|
|
||
|
// First, merge the developer key and bodyObject into the parameters.
|
||
|
|
||
|
NSString *apiKey = self.APIKey;
|
||
|
NSUInteger apiKeyLen = [apiKey length];
|
||
|
|
||
|
NSString *const kDeveloperAPIParamKey = @"key";
|
||
|
NSString *const kBodyObjectParamKey = @"resource";
|
||
|
|
||
|
NSDictionary *finalParams;
|
||
|
if ((apiKeyLen == 0) && (bodyObject == nil)) {
|
||
|
// Nothing needs to be added, just send the dict along.
|
||
|
finalParams = parameters;
|
||
|
} else {
|
||
|
NSMutableDictionary *worker = [NSMutableDictionary dictionary];
|
||
|
if ([parameters count] > 0) {
|
||
|
[worker addEntriesFromDictionary:parameters];
|
||
|
}
|
||
|
if ((apiKeyLen > 0)
|
||
|
&& ([worker objectForKey:kDeveloperAPIParamKey] == nil)) {
|
||
|
[worker setObject:apiKey forKey:kDeveloperAPIParamKey];
|
||
|
}
|
||
|
if (bodyObject != nil) {
|
||
|
GTL_DEBUG_ASSERT([parameters objectForKey:kBodyObjectParamKey] == nil,
|
||
|
@"There was already something under the 'data' key?!");
|
||
|
[worker setObject:[bodyObject JSON] forKey:kBodyObjectParamKey];
|
||
|
}
|
||
|
finalParams = worker;
|
||
|
}
|
||
|
|
||
|
// Now, build up the full dictionary for the JSON-RPC (this is the body of
|
||
|
// the HTTP PUT).
|
||
|
|
||
|
// Spec calls for the jsonrpc entry. Google doesn't require it, but include
|
||
|
// it so the code can work with other servers.
|
||
|
NSMutableDictionary *rpcPayload = [NSMutableDictionary dictionaryWithObjectsAndKeys:
|
||
|
@"2.0", @"jsonrpc",
|
||
|
methodName, @"method",
|
||
|
requestID, @"id",
|
||
|
nil];
|
||
|
|
||
|
// Google extension, provide the version of the api.
|
||
|
NSString *apiVersion = self.apiVersion;
|
||
|
if ([apiVersion length] > 0) {
|
||
|
[rpcPayload setObject:apiVersion forKey:@"apiVersion"];
|
||
|
}
|
||
|
|
||
|
if ([finalParams count] > 0) {
|
||
|
[rpcPayload setObject:finalParams forKey:@"params"];
|
||
|
}
|
||
|
|
||
|
return rpcPayload;
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithMethodNamed:(NSString *)methodName
|
||
|
objectClass:(Class)objectClass
|
||
|
parameters:(NSDictionary *)parameters
|
||
|
bodyObject:(GTLObject *)bodyObject
|
||
|
requestID:(NSString *)requestID
|
||
|
urlQueryParameters:(NSDictionary *)urlQueryParameters
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector
|
||
|
completionHandler:(id)completionHandler // GTLServiceCompletionHandler
|
||
|
executingQuery:(id<GTLQueryProtocol>)executingQuery
|
||
|
ticket:(GTLServiceTicket *)ticket {
|
||
|
GTL_DEBUG_ASSERT([methodName length] > 0, @"Got an empty method name");
|
||
|
if ([methodName length] == 0) return nil;
|
||
|
|
||
|
// If we didn't get a requestID, assign one (call came from one of the public
|
||
|
// calls that doesn't take a GTLQuery object).
|
||
|
if (requestID == nil) {
|
||
|
requestID = [GTLQuery nextRequestID];
|
||
|
}
|
||
|
|
||
|
NSData *dataToPost = nil;
|
||
|
GTLUploadParameters *uploadParameters = executingQuery.uploadParameters;
|
||
|
BOOL shouldSendBody = !uploadParameters.shouldSendUploadOnly;
|
||
|
if (shouldSendBody) {
|
||
|
NSDictionary *rpcPayload = [self rpcPayloadForMethodNamed:methodName
|
||
|
parameters:parameters
|
||
|
bodyObject:bodyObject
|
||
|
requestID:requestID];
|
||
|
|
||
|
NSError *error = nil;
|
||
|
dataToPost = [GTLJSONParser dataWithObject:rpcPayload
|
||
|
humanReadable:NO
|
||
|
error:&error];
|
||
|
if (dataToPost == nil) {
|
||
|
// There is the chance something went into parameters that wasn't valid.
|
||
|
GTL_DEBUG_LOG(@"JSON generation error: %@", error);
|
||
|
return nil;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
BOOL isUploading = (uploadParameters != nil);
|
||
|
NSURL *rpcURL = (isUploading ? self.rpcUploadURL : self.rpcURL);
|
||
|
|
||
|
if ([urlQueryParameters count] > 0) {
|
||
|
rpcURL = [GTLUtilities URLWithString:[rpcURL absoluteString]
|
||
|
queryParameters:urlQueryParameters];
|
||
|
}
|
||
|
|
||
|
BOOL mayAuthorize = (executingQuery ?
|
||
|
!executingQuery.shouldSkipAuthorization : YES);
|
||
|
|
||
|
GTLServiceTicket *resultTicket = [self fetchObjectWithURL:rpcURL
|
||
|
objectClass:objectClass
|
||
|
bodyObject:bodyObject
|
||
|
dataToPost:dataToPost
|
||
|
ETag:nil
|
||
|
httpMethod:@"POST"
|
||
|
mayAuthorize:mayAuthorize
|
||
|
isREST:NO
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:completionHandler
|
||
|
executingQuery:executingQuery
|
||
|
ticket:ticket];
|
||
|
|
||
|
// Set the fetcher log comment to default to the method name
|
||
|
NSUInteger pageNumber = resultTicket.pagesFetchedCounter;
|
||
|
if (pageNumber == 0) {
|
||
|
resultTicket.objectFetcher.comment = methodName;
|
||
|
} else {
|
||
|
// Also put the page number in the log comment
|
||
|
[resultTicket.objectFetcher setCommentWithFormat:@"%@ (page %lu)",
|
||
|
methodName, (unsigned long) (pageNumber + 1)];
|
||
|
}
|
||
|
|
||
|
return resultTicket;
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)executeBatchQuery:(GTLBatchQuery *)batch
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector
|
||
|
completionHandler:(id)completionHandler // GTLServiceCompletionHandler
|
||
|
ticket:(GTLServiceTicket *)ticket {
|
||
|
GTLBatchQuery *batchCopy = [[batch copy] autorelease];
|
||
|
NSArray *queries = batchCopy.queries;
|
||
|
NSUInteger numberOfQueries = [queries count];
|
||
|
if (numberOfQueries == 0) return nil;
|
||
|
|
||
|
// Build up the array of RPC calls.
|
||
|
NSMutableArray *rpcPayloads = [NSMutableArray arrayWithCapacity:numberOfQueries];
|
||
|
NSMutableArray *requestIDs = [NSMutableSet setWithCapacity:numberOfQueries];
|
||
|
for (GTLQuery *query in queries) {
|
||
|
NSString *methodName = query.methodName;
|
||
|
NSDictionary *parameters = query.JSON;
|
||
|
GTLObject *bodyObject = query.bodyObject;
|
||
|
NSString *requestID = query.requestID;
|
||
|
|
||
|
if ([methodName length] == 0 || [requestID length] == 0) {
|
||
|
GTL_DEBUG_ASSERT(0, @"Invalid query - id:%@ method:%@",
|
||
|
requestID, methodName);
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
GTL_DEBUG_ASSERT(query.additionalHTTPHeaders == nil,
|
||
|
@"additionalHTTPHeaders disallowed on queries added to a batch - query %@ (%@)",
|
||
|
requestID, methodName);
|
||
|
|
||
|
GTL_DEBUG_ASSERT(query.uploadParameters == nil,
|
||
|
@"uploadParameters disallowed on queries added to a batch - query %@ (%@)",
|
||
|
requestID, methodName);
|
||
|
|
||
|
NSDictionary *rpcPayload = [self rpcPayloadForMethodNamed:methodName
|
||
|
parameters:parameters
|
||
|
bodyObject:bodyObject
|
||
|
requestID:requestID];
|
||
|
[rpcPayloads addObject:rpcPayload];
|
||
|
|
||
|
if ([requestIDs containsObject:requestID]) {
|
||
|
GTL_DEBUG_LOG(@"Duplicate request id in batch: %@", requestID);
|
||
|
return nil;
|
||
|
}
|
||
|
[requestIDs addObject:requestID];
|
||
|
}
|
||
|
|
||
|
NSError *error = nil;
|
||
|
NSData *dataToPost = nil;
|
||
|
dataToPost = [GTLJSONParser dataWithObject:rpcPayloads
|
||
|
humanReadable:NO
|
||
|
error:&error];
|
||
|
if (dataToPost == nil) {
|
||
|
// There is the chance something went into parameters that wasn't valid.
|
||
|
GTL_DEBUG_LOG(@"JSON generation error: %@", error);
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
BOOL mayAuthorize = (batchCopy ? !batchCopy.shouldSkipAuthorization : YES);
|
||
|
|
||
|
// urlQueryParameters on the queries are currently unsupport during a batch
|
||
|
// as it's not clear how to map them.
|
||
|
|
||
|
NSURL *rpcURL = self.rpcURL;
|
||
|
GTLServiceTicket *resultTicket = [self fetchObjectWithURL:rpcURL
|
||
|
objectClass:[GTLBatchResult class]
|
||
|
bodyObject:nil
|
||
|
dataToPost:dataToPost
|
||
|
ETag:nil
|
||
|
httpMethod:@"POST"
|
||
|
mayAuthorize:mayAuthorize
|
||
|
isREST:NO
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:completionHandler
|
||
|
executingQuery:batch
|
||
|
ticket:ticket];
|
||
|
|
||
|
#if !STRIP_GTM_FETCH_LOGGING
|
||
|
// Set the fetcher log comment
|
||
|
//
|
||
|
// Because this has expensive set operations, it's conditionally
|
||
|
// compiled in
|
||
|
NSArray *methodNames = [queries valueForKey:@"methodName"];
|
||
|
methodNames = [[NSSet setWithArray:methodNames] allObjects]; // de-dupe
|
||
|
NSString *methodsStr = [methodNames componentsJoinedByString:@", "];
|
||
|
|
||
|
NSUInteger pageNumber = ticket.pagesFetchedCounter;
|
||
|
NSString *pageStr = @"";
|
||
|
if (pageNumber > 0) {
|
||
|
pageStr = [NSString stringWithFormat:@"page %lu, ",
|
||
|
(unsigned long) pageNumber + 1];
|
||
|
}
|
||
|
[resultTicket.objectFetcher setCommentWithFormat:@"batch: %@ (%@%lu queries)",
|
||
|
methodsStr, pageStr, (unsigned long) numberOfQueries];
|
||
|
#endif
|
||
|
|
||
|
return resultTicket;
|
||
|
}
|
||
|
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
// REST fetch methods
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL
|
||
|
objectClass:(Class)objectClass
|
||
|
bodyObject:(GTLObject *)bodyObject
|
||
|
ETag:(NSString *)etag
|
||
|
httpMethod:(NSString *)httpMethod
|
||
|
mayAuthorize:(BOOL)mayAuthorize
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector
|
||
|
completionHandler:(id)completionHandler // GTLServiceCompletionHandler
|
||
|
ticket:(GTLServiceTicket *)ticket {
|
||
|
// if no URL was supplied, treat this as if the fetch failed (below)
|
||
|
// and immediately return a nil ticket, skipping the callbacks
|
||
|
//
|
||
|
// this might be considered normal (say, updating a read-only entry
|
||
|
// that lacks an edit link) though higher-level calls may assert or
|
||
|
// return errors depending on the specific usage
|
||
|
if (targetURL == nil) return nil;
|
||
|
|
||
|
NSData *dataToPost = nil;
|
||
|
if (bodyObject != nil) {
|
||
|
NSError *error = nil;
|
||
|
|
||
|
NSDictionary *whatToSend;
|
||
|
NSDictionary *json = bodyObject.JSON;
|
||
|
if (isRESTDataWrapperRequired_) {
|
||
|
// create the top-level "data" object
|
||
|
NSDictionary *dataDict = [NSDictionary dictionaryWithObject:json
|
||
|
forKey:@"data"];
|
||
|
whatToSend = dataDict;
|
||
|
} else {
|
||
|
whatToSend = json;
|
||
|
}
|
||
|
dataToPost = [GTLJSONParser dataWithObject:whatToSend
|
||
|
humanReadable:NO
|
||
|
error:&error];
|
||
|
if (dataToPost == nil) {
|
||
|
GTL_DEBUG_LOG(@"JSON generation error: %@", error);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return [self fetchObjectWithURL:targetURL
|
||
|
objectClass:objectClass
|
||
|
bodyObject:bodyObject
|
||
|
dataToPost:dataToPost
|
||
|
ETag:etag
|
||
|
httpMethod:httpMethod
|
||
|
mayAuthorize:mayAuthorize
|
||
|
isREST:YES
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:completionHandler
|
||
|
executingQuery:nil
|
||
|
ticket:ticket];
|
||
|
}
|
||
|
|
||
|
- (void)invokeProgressCallbackForTicket:(GTLServiceTicket *)ticket
|
||
|
deliveredBytes:(unsigned long long)numReadSoFar
|
||
|
totalBytes:(unsigned long long)total {
|
||
|
|
||
|
SEL progressSelector = [ticket uploadProgressSelector];
|
||
|
if (progressSelector) {
|
||
|
|
||
|
GTMHTTPFetcher *fetcher = ticket.objectFetcher;
|
||
|
id delegate = [fetcher propertyForKey:kFetcherDelegateKey];
|
||
|
|
||
|
NSMethodSignature *signature = [delegate methodSignatureForSelector:progressSelector];
|
||
|
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||
|
|
||
|
[invocation setSelector:progressSelector];
|
||
|
[invocation setTarget:delegate];
|
||
|
[invocation setArgument:&ticket atIndex:2];
|
||
|
[invocation setArgument:&numReadSoFar atIndex:3];
|
||
|
[invocation setArgument:&total atIndex:4];
|
||
|
[invocation invoke];
|
||
|
}
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
GTLServiceUploadProgressBlock block = ticket.uploadProgressBlock;
|
||
|
if (block) {
|
||
|
block(ticket, numReadSoFar, total);
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
// sentData callback from fetcher
|
||
|
- (void)objectFetcher:(GTMHTTPFetcher *)fetcher
|
||
|
didSendBytes:(NSInteger)bytesSent
|
||
|
totalBytesSent:(NSInteger)totalBytesSent
|
||
|
totalBytesExpectedToSend:(NSInteger)totalBytesExpected {
|
||
|
|
||
|
GTLServiceTicket *ticket = [fetcher propertyForKey:kFetcherTicketKey];
|
||
|
|
||
|
[self invokeProgressCallbackForTicket:ticket
|
||
|
deliveredBytes:(unsigned long long)totalBytesSent
|
||
|
totalBytes:(unsigned long long)totalBytesExpected];
|
||
|
}
|
||
|
|
||
|
- (void)objectFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
|
||
|
// we now have the JSON data for an object, or an error
|
||
|
if (error == nil) {
|
||
|
if ([data length] > 0) {
|
||
|
[self prepareToParseObjectForFetcher:fetcher];
|
||
|
} else {
|
||
|
// no data (such as when deleting)
|
||
|
[self handleParsedObjectForFetcher:fetcher];
|
||
|
}
|
||
|
} else {
|
||
|
// There was an error from the fetch
|
||
|
NSInteger status = [error code];
|
||
|
if (status >= 300) {
|
||
|
// Return the HTTP error status code along with a more descriptive error
|
||
|
// from within the HTTP response payload.
|
||
|
NSData *responseData = fetcher.downloadedData;
|
||
|
if ([responseData length] > 0) {
|
||
|
NSDictionary *responseHeaders = fetcher.responseHeaders;
|
||
|
NSString *contentType = [responseHeaders objectForKey:@"Content-Type"];
|
||
|
|
||
|
if ([data length] > 0) {
|
||
|
if ([contentType hasPrefix:@"application/json"]) {
|
||
|
NSError *parseError = nil;
|
||
|
NSMutableDictionary *jsonWrapper = [GTLJSONParser objectWithData:data
|
||
|
error:&parseError];
|
||
|
if (parseError) {
|
||
|
// We could not parse the JSON payload
|
||
|
error = parseError;
|
||
|
} else {
|
||
|
// Convert the JSON error payload into a structured error
|
||
|
NSMutableDictionary *errorJSON = [jsonWrapper valueForKey:@"error"];
|
||
|
GTLErrorObject *errorObject = [GTLErrorObject objectWithJSON:errorJSON];
|
||
|
error = [errorObject foundationError];
|
||
|
}
|
||
|
} else {
|
||
|
// No structured JSON error was available; make a plaintext server
|
||
|
// error response visible in the error object.
|
||
|
NSString *reasonStr = [[[NSString alloc] initWithData:data
|
||
|
encoding:NSUTF8StringEncoding] autorelease];
|
||
|
NSDictionary *userInfo = [NSDictionary dictionaryWithObject:reasonStr
|
||
|
forKey:NSLocalizedFailureReasonErrorKey];
|
||
|
error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
|
||
|
code:status
|
||
|
userInfo:userInfo];
|
||
|
}
|
||
|
} else {
|
||
|
// Response data length is zero; we'll settle for returning the
|
||
|
// fetcher's error.
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// store the error, call the callbacks, and bail
|
||
|
[fetcher setProperty:error
|
||
|
forKey:kFetcherFetchErrorKey];
|
||
|
|
||
|
[self handleParsedObjectForFetcher:fetcher];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Three methods handle parsing of the fetched JSON data:
|
||
|
// - prepareToParse posts a start notification and then spawns off parsing
|
||
|
// on the operation queue (if there's an operation queue)
|
||
|
// - parseObject does the parsing of the JSON string
|
||
|
// - handleParsedObject posts the stop notification and calls the callback
|
||
|
// with the parsed object or an error
|
||
|
//
|
||
|
// The middle method may run on a separate thread.
|
||
|
|
||
|
- (void)prepareToParseObjectForFetcher:(GTMHTTPFetcher *)fetcher {
|
||
|
// save the current thread into the fetcher, since we'll handle additional
|
||
|
// fetches and callbacks on this thread
|
||
|
[fetcher setProperty:[NSThread currentThread]
|
||
|
forKey:kFetcherCallbackThreadKey];
|
||
|
|
||
|
// copy the run loop modes, if any, so we don't need to access them
|
||
|
// from the parsing thread
|
||
|
[fetcher setProperty:[[[self runLoopModes] copy] autorelease]
|
||
|
forKey:kFetcherCallbackRunLoopModesKey];
|
||
|
|
||
|
// we post parsing notifications now to ensure they're on caller's
|
||
|
// original thread
|
||
|
GTLServiceTicket *ticket = [fetcher propertyForKey:kFetcherTicketKey];
|
||
|
NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter];
|
||
|
[defaultNC postNotificationName:kGTLServiceTicketParsingStartedNotification
|
||
|
object:ticket];
|
||
|
[fetcher setProperty:@"1"
|
||
|
forKey:kFetcherParsingNotificationKey];
|
||
|
|
||
|
id<GTLQueryProtocol> executingQuery = ticket.executingQuery;
|
||
|
if ([executingQuery isBatchQuery]) {
|
||
|
// build a dictionary of expected classes for the batch responses
|
||
|
GTLBatchQuery *batchQuery = executingQuery;
|
||
|
NSArray *queries = batchQuery.queries;
|
||
|
NSDictionary *batchClassMap = [NSMutableDictionary dictionaryWithCapacity:[queries count]];
|
||
|
for (GTLQuery *query in queries) {
|
||
|
[batchClassMap setValue:query.expectedObjectClass
|
||
|
forKey:query.requestID];
|
||
|
}
|
||
|
[fetcher setProperty:batchClassMap
|
||
|
forKey:kFetcherBatchClassMapKey];
|
||
|
}
|
||
|
|
||
|
// if there's an operation queue, then use that to schedule parsing on another
|
||
|
// thread
|
||
|
SEL parseSel = @selector(parseObjectFromDataOfFetcher:);
|
||
|
NSOperationQueue *queue = self.parseQueue;
|
||
|
if (queue) {
|
||
|
NSInvocationOperation *op;
|
||
|
op = [[[NSInvocationOperation alloc] initWithTarget:self
|
||
|
selector:parseSel
|
||
|
object:fetcher] autorelease];
|
||
|
ticket.parseOperation = op;
|
||
|
[queue addOperation:op];
|
||
|
// the fetcher now belongs to the parsing thread
|
||
|
} else {
|
||
|
// parse on the current thread, on Mac OS X 10.4 through 10.5.7
|
||
|
// or when the project defines GTL_SKIP_PARSE_THREADING
|
||
|
[self performSelector:parseSel
|
||
|
withObject:fetcher];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- (void)parseObjectFromDataOfFetcher:(GTMHTTPFetcher *)fetcher {
|
||
|
// This method runs in a separate thread
|
||
|
|
||
|
// Generally protect the fetcher properties, since canceling a ticket would
|
||
|
// release the fetcher properties dictionary
|
||
|
NSMutableDictionary *properties = [[fetcher.properties retain] autorelease];
|
||
|
|
||
|
// The callback thread is retaining the fetcher, so the fetcher shouldn't keep
|
||
|
// retaining the callback thread
|
||
|
NSThread *callbackThread = [properties valueForKey:kFetcherCallbackThreadKey];
|
||
|
[[callbackThread retain] autorelease];
|
||
|
[properties removeObjectForKey:kFetcherCallbackThreadKey];
|
||
|
|
||
|
GTLServiceTicket *ticket = [properties valueForKey:kFetcherTicketKey];
|
||
|
[[ticket retain] autorelease];
|
||
|
|
||
|
NSDictionary *responseHeaders = fetcher.responseHeaders;
|
||
|
NSString *contentType = [responseHeaders objectForKey:@"Content-Type"];
|
||
|
NSData *data = fetcher.downloadedData;
|
||
|
|
||
|
NSOperation *parseOperation = ticket.parseOperation;
|
||
|
|
||
|
GTL_DEBUG_ASSERT([contentType hasPrefix:@"application/json"],
|
||
|
@"Got unexpected content type '%@'", contentType);
|
||
|
if ([contentType hasPrefix:@"application/json"] && [data length] > 0) {
|
||
|
#if GTL_LOG_PERFORMANCE
|
||
|
NSTimeInterval secs1, secs2;
|
||
|
secs1 = [NSDate timeIntervalSinceReferenceDate];
|
||
|
#endif
|
||
|
|
||
|
NSError *parseError = nil;
|
||
|
NSMutableDictionary *jsonWrapper = [GTLJSONParser objectWithData:data
|
||
|
error:&parseError];
|
||
|
if ([parseOperation isCancelled]) return;
|
||
|
|
||
|
if (parseError != nil) {
|
||
|
[properties setValue:parseError forKey:kFetcherFetchErrorKey];
|
||
|
} else {
|
||
|
NSMutableDictionary *json;
|
||
|
NSDictionary *batchClassMap = nil;
|
||
|
|
||
|
// In theory, just checking for "application/json-rpc" vs
|
||
|
// "application/json" would work. But the JSON-RPC spec allows for
|
||
|
// "application/json" also so we have to carry a flag all the way in
|
||
|
// saying which type of result to expect and process as.
|
||
|
BOOL isREST = ticket.isREST;
|
||
|
if (isREST) {
|
||
|
if (isRESTDataWrapperRequired_) {
|
||
|
json = [jsonWrapper valueForKey:@"data"];
|
||
|
} else {
|
||
|
json = jsonWrapper;
|
||
|
}
|
||
|
} else {
|
||
|
batchClassMap = [properties valueForKey:kFetcherBatchClassMapKey];
|
||
|
if (batchClassMap) {
|
||
|
// A batch gets the whole array as it's json.
|
||
|
json = jsonWrapper;
|
||
|
} else {
|
||
|
json = [jsonWrapper valueForKey:@"result"];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (json != nil) {
|
||
|
Class defaultClass = [properties valueForKey:kFetcherObjectClassKey];
|
||
|
NSDictionary *surrogates = ticket.surrogates;
|
||
|
|
||
|
GTLObject *parsedObject = [GTLObject objectForJSON:json
|
||
|
defaultClass:defaultClass
|
||
|
surrogates:surrogates
|
||
|
batchClassMap:batchClassMap];
|
||
|
|
||
|
[properties setValue:parsedObject forKey:kFetcherParsedObjectKey];
|
||
|
} else if (!isREST) {
|
||
|
NSMutableDictionary *errorJSON = [jsonWrapper valueForKey:@"error"];
|
||
|
GTL_DEBUG_ASSERT(errorJSON != nil, @"no result or error in response:\n%@",
|
||
|
jsonWrapper);
|
||
|
GTLErrorObject *errorObject = [GTLErrorObject objectWithJSON:errorJSON];
|
||
|
NSError *error = [errorObject foundationError];
|
||
|
|
||
|
// Store the error and let it go to the callback
|
||
|
[properties setValue:error
|
||
|
forKey:kFetcherFetchErrorKey];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#if GTL_LOG_PERFORMANCE
|
||
|
secs2 = [NSDate timeIntervalSinceReferenceDate];
|
||
|
NSLog(@"allocation of %@ took %f seconds", objectClass, secs2 - secs1);
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
if ([parseOperation isCancelled]) return;
|
||
|
|
||
|
SEL parseDoneSel = @selector(handleParsedObjectForFetcher:);
|
||
|
NSArray *runLoopModes = [properties valueForKey:kFetcherCallbackRunLoopModesKey];
|
||
|
if (runLoopModes) {
|
||
|
[self performSelector:parseDoneSel
|
||
|
onThread:callbackThread
|
||
|
withObject:fetcher
|
||
|
waitUntilDone:NO
|
||
|
modes:runLoopModes];
|
||
|
} else {
|
||
|
// defaults to common modes
|
||
|
[self performSelector:parseDoneSel
|
||
|
onThread:callbackThread
|
||
|
withObject:fetcher
|
||
|
waitUntilDone:NO];
|
||
|
}
|
||
|
|
||
|
// the fetcher now belongs to the callback thread
|
||
|
}
|
||
|
|
||
|
- (void)handleParsedObjectForFetcher:(GTMHTTPFetcher *)fetcher {
|
||
|
// After parsing is complete, this is invoked on the thread that the
|
||
|
// fetch was performed on
|
||
|
//
|
||
|
// There may not be an object due to a fetch or parsing error
|
||
|
|
||
|
GTLServiceTicket *ticket = [fetcher propertyForKey:kFetcherTicketKey];
|
||
|
ticket.parseOperation = nil;
|
||
|
|
||
|
// unpack the callback parameters
|
||
|
id delegate = [fetcher propertyForKey:kFetcherDelegateKey];
|
||
|
NSString *selString = [fetcher propertyForKey:kFetcherFinishedSelectorKey];
|
||
|
SEL finishedSelector = NSSelectorFromString(selString);
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
GTLServiceCompletionHandler completionHandler;
|
||
|
completionHandler = [fetcher propertyForKey:kFetcherCompletionHandlerKey];
|
||
|
#else
|
||
|
id completionHandler = nil;
|
||
|
#endif
|
||
|
|
||
|
GTLObject *object = [fetcher propertyForKey:kFetcherParsedObjectKey];
|
||
|
NSError *error = [fetcher propertyForKey:kFetcherFetchErrorKey];
|
||
|
|
||
|
GTLQuery *executingQuery = (GTLQuery *)ticket.executingQuery;
|
||
|
|
||
|
BOOL shouldFetchNextPages = ticket.shouldFetchNextPages;
|
||
|
GTLObject *previousObject = ticket.fetchedObject;
|
||
|
|
||
|
if (shouldFetchNextPages
|
||
|
&& (previousObject != nil)
|
||
|
&& (object != nil)) {
|
||
|
// Accumulate new results
|
||
|
object = [self mergedNewResultObject:object
|
||
|
oldResultObject:previousObject
|
||
|
forQuery:executingQuery];
|
||
|
}
|
||
|
|
||
|
ticket.fetchedObject = object;
|
||
|
ticket.fetchError = error;
|
||
|
|
||
|
if ([fetcher propertyForKey:kFetcherParsingNotificationKey] != nil) {
|
||
|
// we want to always balance the start and stop notifications
|
||
|
NSNotificationCenter *defaultNC = [NSNotificationCenter defaultCenter];
|
||
|
[defaultNC postNotificationName:kGTLServiceTicketParsingStoppedNotification
|
||
|
object:ticket];
|
||
|
}
|
||
|
|
||
|
BOOL shouldCallCallbacks = YES;
|
||
|
|
||
|
// Use the nextPageToken to fetch any later pages for non-batch queries
|
||
|
//
|
||
|
// This assumes a pagination model where objects have entries in an "items"
|
||
|
// field and a "nextPageToken" field, and queries support a "pageToken"
|
||
|
// parameter.
|
||
|
if (ticket.shouldFetchNextPages) {
|
||
|
// Determine if we should fetch more pages of results
|
||
|
|
||
|
GTLQuery *nextPageQuery = [self nextPageQueryForQuery:executingQuery
|
||
|
result:object
|
||
|
ticket:ticket];
|
||
|
if (nextPageQuery) {
|
||
|
BOOL isFetchingMore = [self fetchNextPageWithQuery:nextPageQuery
|
||
|
delegate:delegate
|
||
|
didFinishedSelector:finishedSelector
|
||
|
completionHandler:completionHandler
|
||
|
ticket:ticket];
|
||
|
if (isFetchingMore) {
|
||
|
shouldCallCallbacks = NO;
|
||
|
}
|
||
|
} else {
|
||
|
// No more page tokens are present
|
||
|
#if DEBUG && !GTL_SKIP_PAGES_WARNING
|
||
|
// Each next page followed to accumulate all pages of a feed takes up to
|
||
|
// a few seconds. When multiple pages are being fetched, that
|
||
|
// usually indicates that a larger page size (that is, more items per
|
||
|
// feed fetched) should be requested.
|
||
|
//
|
||
|
// To avoid fetching many pages, set query.maxResults so the feed
|
||
|
// requested is large enough to rarely need to follow next links.
|
||
|
NSUInteger pageCount = ticket.pagesFetchedCounter;
|
||
|
if (pageCount > 2) {
|
||
|
NSString *queryLabel = [executingQuery isBatchQuery] ?
|
||
|
@"batch query" : executingQuery.methodName;
|
||
|
NSLog(@"Executing %@ required fetching %u pages; use a query with a"
|
||
|
@" larger maxResults for faster results",
|
||
|
queryLabel, (unsigned int) pageCount);
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We no longer care about the queries for page 2 or later, so for the client
|
||
|
// inspecting the ticket in the callback, the executing query should be
|
||
|
// the original one
|
||
|
ticket.executingQuery = ticket.originalQuery;
|
||
|
|
||
|
if (shouldCallCallbacks) {
|
||
|
// First, call query-specific callback blocks. We do this before the
|
||
|
// fetch callback to let applications do any final clean-up (or update
|
||
|
// their UI) in the fetch callback.
|
||
|
GTLQuery *originalQuery = (GTLQuery *)ticket.originalQuery;
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
if (![originalQuery isBatchQuery]) {
|
||
|
// Single query
|
||
|
GTLServiceCompletionHandler completionBlock = originalQuery.completionBlock;
|
||
|
if (completionBlock) {
|
||
|
completionBlock(ticket, object, error);
|
||
|
}
|
||
|
} else {
|
||
|
// Batch query
|
||
|
//
|
||
|
// We'll step through the queries of the original batch, not of the
|
||
|
// batch result
|
||
|
GTLBatchQuery *batchQuery = (GTLBatchQuery *)originalQuery;
|
||
|
GTLBatchResult *batchResult = (GTLBatchResult *)object;
|
||
|
NSDictionary *successes = batchResult.successes;
|
||
|
NSDictionary *failures = batchResult.failures;
|
||
|
|
||
|
for (GTLQuery *oneQuery in batchQuery.queries) {
|
||
|
GTLServiceCompletionHandler completionBlock = oneQuery.completionBlock;
|
||
|
if (completionBlock) {
|
||
|
// If there was no networking error, look for a query-specific
|
||
|
// error or result
|
||
|
GTLObject *oneResult = nil;
|
||
|
NSError *oneError = error;
|
||
|
if (oneError == nil) {
|
||
|
NSString *requestID = [oneQuery requestID];
|
||
|
GTLErrorObject *gtlError = [failures objectForKey:requestID];
|
||
|
if (gtlError) {
|
||
|
oneError = [gtlError foundationError];
|
||
|
} else {
|
||
|
oneResult = [successes objectForKey:requestID];
|
||
|
if (oneResult == nil) {
|
||
|
// We found neither a success nor a failure for this
|
||
|
// query, unexpectedly
|
||
|
GTL_DEBUG_LOG(@"GTLService: Batch result missing for request %@",
|
||
|
requestID);
|
||
|
oneError = [NSError errorWithDomain:kGTLServiceErrorDomain
|
||
|
code:kGTLErrorQueryResultMissing
|
||
|
userInfo:nil];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
completionBlock(ticket, oneResult, oneError);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
// Release query callback blocks
|
||
|
[originalQuery executionDidStop];
|
||
|
|
||
|
if (finishedSelector) {
|
||
|
[[self class] invokeCallback:finishedSelector
|
||
|
target:delegate
|
||
|
ticket:ticket
|
||
|
object:object
|
||
|
error:error];
|
||
|
}
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
if (completionHandler) {
|
||
|
completionHandler(ticket, object, error);
|
||
|
}
|
||
|
#endif
|
||
|
ticket.hasCalledCallback = YES;
|
||
|
}
|
||
|
fetcher.properties = nil;
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
// Tickets don't know when the fetch has completed, so the service will
|
||
|
// release their blocks here to avoid unintended retain loops
|
||
|
ticket.retryBlock = nil;
|
||
|
ticket.uploadProgressBlock = nil;
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
+ (void)invokeCallback:(SEL)callbackSel
|
||
|
target:(id)target
|
||
|
ticket:(id)ticket
|
||
|
object:(id)object
|
||
|
error:(id)error {
|
||
|
|
||
|
// GTL fetch callbacks have no return value
|
||
|
NSMethodSignature *signature = [target methodSignatureForSelector:callbackSel];
|
||
|
NSInvocation *retryInvocation = [NSInvocation invocationWithMethodSignature:signature];
|
||
|
[retryInvocation setSelector:callbackSel];
|
||
|
[retryInvocation setTarget:target];
|
||
|
[retryInvocation setArgument:&ticket atIndex:2];
|
||
|
[retryInvocation setArgument:&object atIndex:3];
|
||
|
[retryInvocation setArgument:&error atIndex:4];
|
||
|
[retryInvocation invoke];
|
||
|
}
|
||
|
|
||
|
// The object fetcher may call into this retry method; this one invokes the
|
||
|
// selector provided by the user.
|
||
|
- (BOOL)objectFetcher:(GTMHTTPFetcher *)fetcher willRetry:(BOOL)willRetry forError:(NSError *)error {
|
||
|
|
||
|
GTLServiceTicket *ticket = [fetcher propertyForKey:kFetcherTicketKey];
|
||
|
SEL retrySelector = ticket.retrySelector;
|
||
|
if (retrySelector) {
|
||
|
id delegate = [fetcher propertyForKey:kFetcherDelegateKey];
|
||
|
|
||
|
willRetry = [self invokeRetrySelector:retrySelector
|
||
|
delegate:delegate
|
||
|
ticket:ticket
|
||
|
willRetry:willRetry
|
||
|
error:error];
|
||
|
}
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
BOOL (^retryBlock)(GTLServiceTicket *, BOOL, NSError *) = ticket.retryBlock;
|
||
|
if (retryBlock) {
|
||
|
willRetry = retryBlock(ticket, willRetry, error);
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
return willRetry;
|
||
|
}
|
||
|
|
||
|
- (BOOL)invokeRetrySelector:(SEL)retrySelector
|
||
|
delegate:(id)delegate
|
||
|
ticket:(GTLServiceTicket *)ticket
|
||
|
willRetry:(BOOL)willRetry
|
||
|
error:(NSError *)error {
|
||
|
|
||
|
if ([delegate respondsToSelector:retrySelector]) {
|
||
|
// Unlike the retry selector invocation in GTMHTTPFetcher, this invocation
|
||
|
// passes the ticket rather than the fetcher as argument 2
|
||
|
NSMethodSignature *signature = [delegate methodSignatureForSelector:retrySelector];
|
||
|
NSInvocation *retryInvocation = [NSInvocation invocationWithMethodSignature:signature];
|
||
|
[retryInvocation setSelector:retrySelector];
|
||
|
[retryInvocation setTarget:delegate];
|
||
|
[retryInvocation setArgument:&ticket atIndex:2]; // ticket passed
|
||
|
[retryInvocation setArgument:&willRetry atIndex:3];
|
||
|
[retryInvocation setArgument:&error atIndex:4];
|
||
|
[retryInvocation invoke];
|
||
|
|
||
|
[retryInvocation getReturnValue:&willRetry];
|
||
|
}
|
||
|
return willRetry;
|
||
|
}
|
||
|
|
||
|
- (BOOL)waitForTicket:(GTLServiceTicket *)ticket
|
||
|
timeout:(NSTimeInterval)timeoutInSeconds
|
||
|
fetchedObject:(GTLObject **)outObjectOrNil
|
||
|
error:(NSError **)outErrorOrNil {
|
||
|
|
||
|
NSDate* giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
|
||
|
|
||
|
// loop until the fetch completes with an object or an error,
|
||
|
// or until the timeout has expired
|
||
|
while (![ticket hasCalledCallback]
|
||
|
&& [giveUpDate timeIntervalSinceNow] > 0) {
|
||
|
|
||
|
// run the current run loop 1/1000 of a second to give the networking
|
||
|
// code a chance to work
|
||
|
NSDate *stopDate = [NSDate dateWithTimeIntervalSinceNow:0.001];
|
||
|
[[NSRunLoop currentRunLoop] runUntilDate:stopDate];
|
||
|
}
|
||
|
|
||
|
NSError *fetchError = ticket.fetchError;
|
||
|
|
||
|
if (![ticket hasCalledCallback] && fetchError == nil) {
|
||
|
fetchError = [NSError errorWithDomain:kGTLServiceErrorDomain
|
||
|
code:kGTLErrorWaitTimedOut
|
||
|
userInfo:nil];
|
||
|
}
|
||
|
|
||
|
if (outObjectOrNil) *outObjectOrNil = ticket.fetchedObject;
|
||
|
if (outErrorOrNil) *outErrorOrNil = fetchError;
|
||
|
|
||
|
return (fetchError == nil);
|
||
|
}
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
// Given a single or batch query and its result, make a new query
|
||
|
// for the next pages, if any. Returns nil if there's no additional
|
||
|
// query to make.
|
||
|
//
|
||
|
// This method calls itself recursively to make the individual next page
|
||
|
// queries for a batch query.
|
||
|
- (id <GTLQueryProtocol>)nextPageQueryForQuery:(GTLQuery *)query
|
||
|
result:(GTLObject *)object
|
||
|
ticket:(GTLServiceTicket *)ticket {
|
||
|
if (!query.isBatchQuery) {
|
||
|
// This is a single query
|
||
|
|
||
|
// Determine if we should fetch more pages of results
|
||
|
GTLQuery *nextPageQuery = nil;
|
||
|
NSString *nextPageToken = nil;
|
||
|
NSNumber *nextStartIndex = nil;
|
||
|
|
||
|
if ([object respondsToSelector:@selector(nextPageToken)]
|
||
|
&& [query respondsToSelector:@selector(pageToken)]) {
|
||
|
nextPageToken = [object performSelector:@selector(nextPageToken)];
|
||
|
}
|
||
|
|
||
|
if ([object respondsToSelector:@selector(nextStartIndex)]
|
||
|
&& [query respondsToSelector:@selector(startIndex)]) {
|
||
|
nextStartIndex = [object performSelector:@selector(nextStartIndex)];
|
||
|
}
|
||
|
|
||
|
if (nextPageToken || nextStartIndex) {
|
||
|
// Make a query for the next page, preserving the request ID
|
||
|
nextPageQuery = [[query copy] autorelease];
|
||
|
nextPageQuery.requestID = query.requestID;
|
||
|
|
||
|
if (nextPageToken) {
|
||
|
[nextPageQuery performSelector:@selector(setPageToken:)
|
||
|
withObject:nextPageToken];
|
||
|
} else {
|
||
|
// Use KVC to unwrap the scalar type instead of converting the
|
||
|
// NSNumber to an integer and using NSInvocation
|
||
|
[nextPageQuery setValue:nextStartIndex
|
||
|
forKey:@"startIndex"];
|
||
|
}
|
||
|
}
|
||
|
return nextPageQuery;
|
||
|
} else {
|
||
|
// This is a batch query
|
||
|
//
|
||
|
// Check if there's a next page to fetch for any of the success
|
||
|
// results by invoking this method recursively on each of those results
|
||
|
GTLBatchResult *batchResult = (GTLBatchResult *)object;
|
||
|
GTLBatchQuery *nextPageBatchQuery = nil;
|
||
|
NSDictionary *successes = batchResult.successes;
|
||
|
|
||
|
for (NSString *requestID in successes) {
|
||
|
GTLObject *singleObject = [successes objectForKey:requestID];
|
||
|
GTLQuery *singleQuery = [ticket queryForRequestID:requestID];
|
||
|
|
||
|
GTLQuery *newQuery = [self nextPageQueryForQuery:singleQuery
|
||
|
result:singleObject
|
||
|
ticket:ticket];
|
||
|
if (newQuery) {
|
||
|
// There is another query to fetch
|
||
|
if (nextPageBatchQuery == nil) {
|
||
|
nextPageBatchQuery = [GTLBatchQuery batchQuery];
|
||
|
}
|
||
|
[nextPageBatchQuery addQuery:newQuery];
|
||
|
}
|
||
|
}
|
||
|
return nextPageBatchQuery;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// When a ticket is set to fetch more pages for feeds, this routine
|
||
|
// initiates the fetch for each additional feed page
|
||
|
- (BOOL)fetchNextPageWithQuery:(GTLQuery *)query
|
||
|
delegate:(id)delegate
|
||
|
didFinishedSelector:(SEL)finishedSelector
|
||
|
completionHandler:(GTLServiceCompletionHandler)completionHandler
|
||
|
ticket:(GTLServiceTicket *)ticket {
|
||
|
// Sanity check the number of pages fetched already
|
||
|
NSUInteger oldPagesFetchedCounter = ticket.pagesFetchedCounter;
|
||
|
|
||
|
if (oldPagesFetchedCounter > kMaxNumberOfNextPagesFetched) {
|
||
|
// Sanity check failed: way too many pages were fetched
|
||
|
//
|
||
|
// The client should be querying with a higher max results per page
|
||
|
// to avoid this
|
||
|
GTL_DEBUG_ASSERT(0, @"Fetched too many next pages for %@",
|
||
|
query.methodName);
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
ticket.pagesFetchedCounter = 1 + oldPagesFetchedCounter;
|
||
|
|
||
|
GTLServiceTicket *newTicket;
|
||
|
if (query.isBatchQuery) {
|
||
|
newTicket = [self executeBatchQuery:(GTLBatchQuery *)query
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:completionHandler
|
||
|
ticket:ticket];
|
||
|
} else {
|
||
|
newTicket = [self fetchObjectWithMethodNamed:query.methodName
|
||
|
objectClass:query.expectedObjectClass
|
||
|
parameters:query.JSON
|
||
|
bodyObject:query.bodyObject
|
||
|
requestID:query.requestID
|
||
|
urlQueryParameters:query.urlQueryParameters
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:completionHandler
|
||
|
executingQuery:query
|
||
|
ticket:ticket];
|
||
|
}
|
||
|
|
||
|
// In the bizarre case that the fetch didn't begin, newTicket will be
|
||
|
// nil. So long as the new ticket is the same as the ticket we're
|
||
|
// continuing, then we're happy.
|
||
|
return (newTicket == ticket);
|
||
|
}
|
||
|
|
||
|
// Given a new single or batch result (meaning additional pages for a previous
|
||
|
// query result), merge it into the old result.
|
||
|
- (GTLObject *)mergedNewResultObject:(GTLObject *)newResult
|
||
|
oldResultObject:(GTLObject *)oldResult
|
||
|
forQuery:(GTLQuery *)query {
|
||
|
if (query.isBatchQuery) {
|
||
|
// Batch query result
|
||
|
//
|
||
|
// The new batch results are a subset of the old result's queries, since
|
||
|
// not all queries in the batch necessarily have additional pages.
|
||
|
//
|
||
|
// New success objects replace old success objects, with the old items
|
||
|
// prepended; new failure objects replace old success objects.
|
||
|
// We will update the old batch results with accumulated items, using the
|
||
|
// new objects, and return the old batch.
|
||
|
//
|
||
|
// We reuse the old batch results object because it may include some earlier
|
||
|
// results which did not have additional pages.
|
||
|
GTLBatchResult *newBatchResult = (GTLBatchResult *)newResult;
|
||
|
GTLBatchResult *oldBatchResult = (GTLBatchResult *)oldResult;
|
||
|
|
||
|
NSMutableDictionary *newSuccesses = newBatchResult.successes;
|
||
|
NSMutableDictionary *newFailures = newBatchResult.failures;
|
||
|
NSMutableDictionary *oldSuccesses = oldBatchResult.successes;
|
||
|
NSMutableDictionary *oldFailures = oldBatchResult.failures;
|
||
|
|
||
|
for (NSString *requestID in newSuccesses) {
|
||
|
// Prepend the old items to the new response's items
|
||
|
//
|
||
|
// We can assume the objects are collections since they're present in
|
||
|
// additional pages.
|
||
|
GTLCollectionObject *newObj = [newSuccesses objectForKey:requestID];
|
||
|
GTLCollectionObject *oldObj = [oldSuccesses objectForKey:requestID];
|
||
|
|
||
|
NSMutableArray *items = [NSMutableArray arrayWithArray:oldObj.items];
|
||
|
[items addObjectsFromArray:newObj.items];
|
||
|
[newObj performSelector:@selector(setItems:) withObject:items];
|
||
|
|
||
|
// Replace the old object with the new one
|
||
|
[oldSuccesses setObject:newObj forKey:requestID];
|
||
|
}
|
||
|
|
||
|
for (NSString *requestID in newFailures) {
|
||
|
// Replace old successes or failures with the new failure
|
||
|
GTLErrorObject *newError = [newFailures objectForKey:requestID];
|
||
|
[oldFailures setObject:newError forKey:requestID];
|
||
|
[oldSuccesses removeObjectForKey:requestID];
|
||
|
}
|
||
|
return oldBatchResult;
|
||
|
} else {
|
||
|
// Single query result
|
||
|
//
|
||
|
// Merge the items into the new object, and return that.
|
||
|
//
|
||
|
// We can assume the objects are collections since they're present in
|
||
|
// additional pages.
|
||
|
GTLCollectionObject *newObj = (GTLCollectionObject *)newResult;
|
||
|
GTLCollectionObject *oldObj = (GTLCollectionObject *)oldResult;
|
||
|
|
||
|
NSMutableArray *items = [NSMutableArray arrayWithArray:oldObj.items];
|
||
|
[items addObjectsFromArray:newObj.items];
|
||
|
[newObj performSelector:@selector(setItems:) withObject:items];
|
||
|
|
||
|
return newObj;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
// GTLQuery methods.
|
||
|
|
||
|
- (GTLServiceTicket *)executeQuery:(id<GTLQueryProtocol>)queryObj
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
if ([queryObj isBatchQuery]) {
|
||
|
return [self executeBatchQuery:queryObj
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:NULL
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
GTLQuery *query = [[(GTLQuery *)queryObj copy] autorelease];
|
||
|
NSString *methodName = query.methodName;
|
||
|
NSDictionary *params = query.JSON;
|
||
|
GTLObject *bodyObject = query.bodyObject;
|
||
|
|
||
|
return [self fetchObjectWithMethodNamed:methodName
|
||
|
objectClass:query.expectedObjectClass
|
||
|
parameters:params
|
||
|
bodyObject:bodyObject
|
||
|
requestID:query.requestID
|
||
|
urlQueryParameters:query.urlQueryParameters
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
executingQuery:query
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
- (GTLServiceTicket *)executeQuery:(id<GTLQueryProtocol>)queryObj
|
||
|
completionHandler:(void (^)(GTLServiceTicket *ticket, id object, NSError *error))handler {
|
||
|
if ([queryObj isBatchQuery]) {
|
||
|
return [self executeBatchQuery:queryObj
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
GTLQuery *query = [[(GTLQuery *)queryObj copy] autorelease];
|
||
|
NSString *methodName = query.methodName;
|
||
|
NSDictionary *params = query.JSON;
|
||
|
GTLObject *bodyObject = query.bodyObject;
|
||
|
|
||
|
return [self fetchObjectWithMethodNamed:methodName
|
||
|
objectClass:query.expectedObjectClass
|
||
|
parameters:params
|
||
|
bodyObject:bodyObject
|
||
|
requestID:query.requestID
|
||
|
urlQueryParameters:query.urlQueryParameters
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
executingQuery:query
|
||
|
ticket:nil];
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithMethodNamed:(NSString *)methodName
|
||
|
parameters:(NSDictionary *)parameters
|
||
|
objectClass:(Class)objectClass
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
return [self fetchObjectWithMethodNamed:methodName
|
||
|
objectClass:objectClass
|
||
|
parameters:parameters
|
||
|
bodyObject:nil
|
||
|
requestID:nil
|
||
|
urlQueryParameters:nil
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
executingQuery:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithMethodNamed:(NSString *)methodName
|
||
|
insertingObject:(GTLObject *)bodyObject
|
||
|
objectClass:(Class)objectClass
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
return [self fetchObjectWithMethodNamed:methodName
|
||
|
objectClass:objectClass
|
||
|
parameters:nil
|
||
|
bodyObject:bodyObject
|
||
|
requestID:nil
|
||
|
urlQueryParameters:nil
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
executingQuery:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithMethodNamed:(NSString *)methodName
|
||
|
parameters:(NSDictionary *)parameters
|
||
|
insertingObject:(GTLObject *)bodyObject
|
||
|
objectClass:(Class)objectClass
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
return [self fetchObjectWithMethodNamed:methodName
|
||
|
objectClass:objectClass
|
||
|
parameters:parameters
|
||
|
bodyObject:bodyObject
|
||
|
requestID:nil
|
||
|
urlQueryParameters:nil
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
executingQuery:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
- (GTLServiceTicket *)fetchObjectWithMethodNamed:(NSString *)methodName
|
||
|
parameters:(NSDictionary *)parameters
|
||
|
objectClass:(Class)objectClass
|
||
|
completionHandler:(void (^)(GTLServiceTicket *ticket, id object, NSError *error))handler {
|
||
|
return [self fetchObjectWithMethodNamed:methodName
|
||
|
objectClass:objectClass
|
||
|
parameters:parameters
|
||
|
bodyObject:nil
|
||
|
requestID:nil
|
||
|
urlQueryParameters:nil
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
executingQuery:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithMethodNamed:(NSString *)methodName
|
||
|
insertingObject:(GTLObject *)bodyObject
|
||
|
objectClass:(Class)objectClass
|
||
|
completionHandler:(void (^)(GTLServiceTicket *ticket, id object, NSError *error))handler {
|
||
|
return [self fetchObjectWithMethodNamed:methodName
|
||
|
objectClass:objectClass
|
||
|
parameters:nil
|
||
|
bodyObject:bodyObject
|
||
|
requestID:nil
|
||
|
urlQueryParameters:nil
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
executingQuery:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithMethodNamed:(NSString *)methodName
|
||
|
parameters:(NSDictionary *)parameters
|
||
|
insertingObject:(GTLObject *)bodyObject
|
||
|
objectClass:(Class)objectClass
|
||
|
completionHandler:(void (^)(GTLServiceTicket *ticket, id object, NSError *error))handler {
|
||
|
return [self fetchObjectWithMethodNamed:methodName
|
||
|
objectClass:objectClass
|
||
|
parameters:parameters
|
||
|
bodyObject:bodyObject
|
||
|
requestID:nil
|
||
|
urlQueryParameters:nil
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
executingQuery:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
// These external entry points doing a REST style fetch.
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithURL:(NSURL *)feedURL
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
// no object class specified; use registered class
|
||
|
return [self fetchObjectWithURL:feedURL
|
||
|
objectClass:nil
|
||
|
bodyObject:nil
|
||
|
ETag:nil
|
||
|
httpMethod:nil
|
||
|
mayAuthorize:YES
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchPublicObjectWithURL:(NSURL *)feedURL
|
||
|
objectClass:(Class)objectClass
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
return [self fetchObjectWithURL:feedURL
|
||
|
objectClass:objectClass
|
||
|
bodyObject:nil
|
||
|
ETag:nil
|
||
|
httpMethod:nil
|
||
|
mayAuthorize:NO
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectWithURL:(NSURL *)feedURL
|
||
|
objectClass:(Class)objectClass
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
return [self fetchObjectWithURL:feedURL
|
||
|
objectClass:objectClass
|
||
|
bodyObject:nil
|
||
|
ETag:nil
|
||
|
httpMethod:nil
|
||
|
mayAuthorize:YES
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectByInsertingObject:(GTLObject *)bodyToPost
|
||
|
forURL:(NSURL *)destinationURL
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
Class objClass = [bodyToPost class];
|
||
|
NSString *etag = ETagIfPresent(bodyToPost);
|
||
|
|
||
|
return [self fetchObjectWithURL:destinationURL
|
||
|
objectClass:objClass
|
||
|
bodyObject:bodyToPost
|
||
|
ETag:etag
|
||
|
httpMethod:@"POST"
|
||
|
mayAuthorize:YES
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectByUpdatingObject:(GTLObject *)bodyToPut
|
||
|
forURL:(NSURL *)destinationURL
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
Class objClass = [bodyToPut class];
|
||
|
NSString *etag = ETagIfPresent(bodyToPut);
|
||
|
|
||
|
return [self fetchObjectWithURL:destinationURL
|
||
|
objectClass:objClass
|
||
|
bodyObject:bodyToPut
|
||
|
ETag:etag
|
||
|
httpMethod:@"PUT"
|
||
|
mayAuthorize:YES
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
|
||
|
- (GTLServiceTicket *)deleteResourceURL:(NSURL *)destinationURL
|
||
|
ETag:(NSString *)etagOrNil
|
||
|
delegate:(id)delegate
|
||
|
didFinishSelector:(SEL)finishedSelector {
|
||
|
return [self fetchObjectWithURL:destinationURL
|
||
|
objectClass:nil
|
||
|
bodyObject:nil
|
||
|
ETag:etagOrNil
|
||
|
httpMethod:@"DELETE"
|
||
|
mayAuthorize:YES
|
||
|
delegate:delegate
|
||
|
didFinishSelector:finishedSelector
|
||
|
completionHandler:nil
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
- (GTLServiceTicket *)fetchObjectWithURL:(NSURL *)objectURL
|
||
|
completionHandler:(void (^)(GTLServiceTicket *ticket, id object, NSError *error))handler {
|
||
|
return [self fetchObjectWithURL:objectURL
|
||
|
objectClass:nil
|
||
|
bodyObject:nil
|
||
|
ETag:nil
|
||
|
httpMethod:nil
|
||
|
mayAuthorize:YES
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectByInsertingObject:(GTLObject *)bodyToPost
|
||
|
forURL:(NSURL *)destinationURL
|
||
|
completionHandler:(void (^)(GTLServiceTicket *ticket, id object, NSError *error))handler {
|
||
|
Class objClass = [bodyToPost class];
|
||
|
NSString *etag = ETagIfPresent(bodyToPost);
|
||
|
|
||
|
return [self fetchObjectWithURL:destinationURL
|
||
|
objectClass:objClass
|
||
|
bodyObject:bodyToPost
|
||
|
ETag:etag
|
||
|
httpMethod:@"POST"
|
||
|
mayAuthorize:YES
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)fetchObjectByUpdatingObject:(GTLObject *)bodyToPut
|
||
|
forURL:(NSURL *)destinationURL
|
||
|
completionHandler:(void (^)(GTLServiceTicket *ticket, id object, NSError *error))handler {
|
||
|
Class objClass = [bodyToPut class];
|
||
|
NSString *etag = ETagIfPresent(bodyToPut);
|
||
|
|
||
|
return [self fetchObjectWithURL:destinationURL
|
||
|
objectClass:objClass
|
||
|
bodyObject:bodyToPut
|
||
|
ETag:etag
|
||
|
httpMethod:@"PUT"
|
||
|
mayAuthorize:YES
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
- (GTLServiceTicket *)deleteResourceURL:(NSURL *)destinationURL
|
||
|
ETag:(NSString *)etagOrNil
|
||
|
completionHandler:(void (^)(GTLServiceTicket *ticket, id object, NSError *error))handler {
|
||
|
return [self fetchObjectWithURL:destinationURL
|
||
|
objectClass:nil
|
||
|
bodyObject:nil
|
||
|
ETag:etagOrNil
|
||
|
httpMethod:@"DELETE"
|
||
|
mayAuthorize:YES
|
||
|
delegate:nil
|
||
|
didFinishSelector:NULL
|
||
|
completionHandler:handler
|
||
|
ticket:nil];
|
||
|
}
|
||
|
|
||
|
#endif // NS_BLOCKS_AVAILABLE
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
- (NSString *)userAgent {
|
||
|
return userAgent_;
|
||
|
}
|
||
|
|
||
|
- (void)setExactUserAgent:(NSString *)userAgent {
|
||
|
// internal use only
|
||
|
[userAgent_ release];
|
||
|
userAgent_ = [userAgent copy];
|
||
|
}
|
||
|
|
||
|
- (void)setUserAgent:(NSString *)userAgent {
|
||
|
// remove whitespace and unfriendly characters
|
||
|
NSString *str = GTMCleanedUserAgentString(userAgent);
|
||
|
[self setExactUserAgent:str];
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// The following methods pass through to the fetcher service object
|
||
|
//
|
||
|
|
||
|
- (void)setCookieStorageMethod:(NSInteger)method {
|
||
|
self.fetcherService.cookieStorageMethod = method;
|
||
|
}
|
||
|
|
||
|
- (NSInteger)cookieStorageMethod {
|
||
|
return self.fetcherService.cookieStorageMethod;
|
||
|
}
|
||
|
|
||
|
- (void)setShouldFetchInBackground:(BOOL)flag {
|
||
|
self.fetcherService.shouldFetchInBackground = flag;
|
||
|
}
|
||
|
|
||
|
- (BOOL)shouldFetchInBackground {
|
||
|
return self.fetcherService.shouldFetchInBackground;
|
||
|
}
|
||
|
|
||
|
- (void)setRunLoopModes:(NSArray *)array {
|
||
|
self.fetcherService.runLoopModes = array;
|
||
|
}
|
||
|
|
||
|
- (NSArray *)runLoopModes {
|
||
|
return self.fetcherService.runLoopModes;
|
||
|
}
|
||
|
|
||
|
#pragma mark -
|
||
|
|
||
|
// The service properties becomes the initial value for each future ticket's
|
||
|
// properties
|
||
|
- (void)setServiceProperties:(NSDictionary *)dict {
|
||
|
[serviceProperties_ autorelease];
|
||
|
serviceProperties_ = [dict mutableCopy];
|
||
|
}
|
||
|
|
||
|
- (NSDictionary *)serviceProperties {
|
||
|
// be sure the returned pointer has the life of the autorelease pool,
|
||
|
// in case self is released immediately
|
||
|
return [[serviceProperties_ retain] autorelease];
|
||
|
}
|
||
|
|
||
|
- (void)setServiceProperty:(id)obj forKey:(NSString *)key {
|
||
|
|
||
|
if (obj == nil) {
|
||
|
// user passed in nil, so delete the property
|
||
|
[serviceProperties_ removeObjectForKey:key];
|
||
|
} else {
|
||
|
// be sure the property dictionary exists
|
||
|
if (serviceProperties_ == nil) {
|
||
|
[self setServiceProperties:[NSDictionary dictionary]];
|
||
|
}
|
||
|
[serviceProperties_ setObject:obj forKey:key];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- (id)servicePropertyForKey:(NSString *)key {
|
||
|
id obj = [serviceProperties_ objectForKey:key];
|
||
|
|
||
|
// be sure the returned pointer has the life of the autorelease pool,
|
||
|
// in case self is released immediately
|
||
|
return [[obj retain] autorelease];
|
||
|
}
|
||
|
|
||
|
- (void)setServiceUserData:(id)userData {
|
||
|
[self setServiceProperty:userData forKey:kUserDataPropertyKey];
|
||
|
}
|
||
|
|
||
|
- (id)serviceUserData {
|
||
|
return [[[self servicePropertyForKey:kUserDataPropertyKey] retain] autorelease];
|
||
|
}
|
||
|
|
||
|
- (void)setAuthorizer:(id <GTMFetcherAuthorizationProtocol>)authorizer {
|
||
|
self.fetcherService.authorizer = authorizer;
|
||
|
}
|
||
|
|
||
|
- (id <GTMFetcherAuthorizationProtocol>)authorizer {
|
||
|
return self.fetcherService.authorizer;
|
||
|
}
|
||
|
|
||
|
+ (NSUInteger)defaultServiceUploadChunkSize {
|
||
|
// subclasses may override
|
||
|
return kStandardUploadChunkSize;
|
||
|
}
|
||
|
|
||
|
- (NSUInteger)serviceUploadChunkSize {
|
||
|
return uploadChunkSize_;
|
||
|
}
|
||
|
|
||
|
- (void)setServiceUploadChunkSize:(NSUInteger)val {
|
||
|
|
||
|
if (val == kGTLStandardUploadChunkSize) {
|
||
|
// determine an appropriate upload chunk size for the system
|
||
|
|
||
|
if (![GTMHTTPFetcher doesSupportSentDataCallback]) {
|
||
|
// for 10.4 and iPhone 2, we need a small upload chunk size so there
|
||
|
// are frequent intrachunk callbacks for progress monitoring
|
||
|
val = 75000;
|
||
|
} else {
|
||
|
#if GTL_IPHONE
|
||
|
val = 1000000;
|
||
|
#else
|
||
|
if (NSFoundationVersionNumber >= 751.00) {
|
||
|
// Mac OS X 10.6
|
||
|
//
|
||
|
// we'll pick a huge upload chunk size, which minimizes http overhead
|
||
|
// and server effort, and we'll hope that NSURLConnection can finally
|
||
|
// handle big uploads reliably
|
||
|
val = 25000000;
|
||
|
} else {
|
||
|
// Mac OS X 10.5
|
||
|
//
|
||
|
// NSURLConnection is more reliable on POSTs in 10.5 than it was in
|
||
|
// 10.4, but it still fails mysteriously on big uploads on some
|
||
|
// systems, so we'll limit the chunks to a megabyte
|
||
|
val = 1000000;
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
}
|
||
|
uploadChunkSize_ = val;
|
||
|
}
|
||
|
|
||
|
@end
|
||
|
|
||
|
@implementation GTLServiceTicket
|
||
|
|
||
|
@synthesize shouldFetchNextPages = shouldFetchNextPages_,
|
||
|
surrogates = surrogates_,
|
||
|
uploadProgressSelector = uploadProgressSelector_,
|
||
|
retryEnabled = isRetryEnabled_,
|
||
|
hasCalledCallback = hasCalledCallback_,
|
||
|
retrySelector = retrySelector_,
|
||
|
maxRetryInterval = maxRetryInterval_,
|
||
|
objectFetcher = objectFetcher_,
|
||
|
postedObject = postedObject_,
|
||
|
fetchedObject = fetchedObject_,
|
||
|
executingQuery = executingQuery_,
|
||
|
originalQuery = originalQuery_,
|
||
|
fetchError = fetchError_,
|
||
|
pagesFetchedCounter = pagesFetchedCounter_,
|
||
|
APIKey = apiKey_,
|
||
|
parseOperation = parseOperation_,
|
||
|
isREST = isREST_;
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
@synthesize retryBlock = retryBlock_;
|
||
|
#endif
|
||
|
|
||
|
+ (id)ticketForService:(GTLService *)service {
|
||
|
return [[[self alloc] initWithService:service] autorelease];
|
||
|
}
|
||
|
|
||
|
- (id)initWithService:(GTLService *)service {
|
||
|
self = [super init];
|
||
|
if (self) {
|
||
|
service_ = [service retain];
|
||
|
|
||
|
ticketProperties_ = [service.serviceProperties mutableCopy];
|
||
|
surrogates_ = [service.surrogates retain];
|
||
|
uploadProgressSelector_ = service.uploadProgressSelector;
|
||
|
isRetryEnabled_ = service.retryEnabled;
|
||
|
retrySelector_ = service.retrySelector;
|
||
|
maxRetryInterval_ = service.maxRetryInterval;
|
||
|
shouldFetchNextPages_ = service.shouldFetchNextPages;
|
||
|
apiKey_ = [service.APIKey copy];
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
uploadProgressBlock_ = [service.uploadProgressBlock copy];
|
||
|
retryBlock_ = [service.retryBlock copy];
|
||
|
#endif
|
||
|
}
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
- (void)dealloc {
|
||
|
[service_ release];
|
||
|
[ticketProperties_ release];
|
||
|
[surrogates_ release];
|
||
|
[objectFetcher_ release];
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
[uploadProgressBlock_ release];
|
||
|
[retryBlock_ release];
|
||
|
#endif
|
||
|
[postedObject_ release];
|
||
|
[fetchedObject_ release];
|
||
|
[executingQuery_ release];
|
||
|
[originalQuery_ release];
|
||
|
[fetchError_ release];
|
||
|
[apiKey_ release];
|
||
|
[parseOperation_ release];
|
||
|
|
||
|
[super dealloc];
|
||
|
}
|
||
|
|
||
|
- (NSString *)description {
|
||
|
NSString *devKeyInfo = @"";
|
||
|
if (apiKey_ != nil) {
|
||
|
devKeyInfo = [NSString stringWithFormat:@" devKey:%@", apiKey_];
|
||
|
}
|
||
|
|
||
|
NSString *authorizerInfo = @"";
|
||
|
id <GTMFetcherAuthorizationProtocol> authorizer = self.objectFetcher.authorizer;
|
||
|
if (authorizer != nil) {
|
||
|
authorizerInfo = [NSString stringWithFormat:@" authorizer:%@", authorizer];
|
||
|
}
|
||
|
|
||
|
return [NSString stringWithFormat:@"%@ %p: {service:%@%@%@ fetcher:%@ }",
|
||
|
[self class], self, service_, devKeyInfo, authorizerInfo, objectFetcher_];
|
||
|
}
|
||
|
|
||
|
- (void)pauseUpload {
|
||
|
BOOL canPause = [objectFetcher_ respondsToSelector:@selector(pauseFetching)];
|
||
|
GTL_DEBUG_ASSERT(canPause, @"unpauseable ticket");
|
||
|
|
||
|
if (canPause) {
|
||
|
[(GTMHTTPUploadFetcher *)objectFetcher_ pauseFetching];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- (void)resumeUpload {
|
||
|
BOOL canResume = [objectFetcher_ respondsToSelector:@selector(resumeFetching)];
|
||
|
GTL_DEBUG_ASSERT(canResume, @"unresumable ticket");
|
||
|
|
||
|
if (canResume) {
|
||
|
[(GTMHTTPUploadFetcher *)objectFetcher_ resumeFetching];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- (BOOL)isUploadPaused {
|
||
|
BOOL isPausable = [objectFetcher_ respondsToSelector:@selector(isPaused)];
|
||
|
GTL_DEBUG_ASSERT(isPausable, @"unpauseable ticket");
|
||
|
|
||
|
if (isPausable) {
|
||
|
return [(GTMHTTPUploadFetcher *)objectFetcher_ isPaused];
|
||
|
}
|
||
|
return NO;
|
||
|
}
|
||
|
|
||
|
- (void)cancelTicket {
|
||
|
NSOperation *parseOperation = self.parseOperation;
|
||
|
[parseOperation cancel];
|
||
|
self.parseOperation = nil;
|
||
|
|
||
|
[objectFetcher_ stopFetching];
|
||
|
objectFetcher_.properties = nil;
|
||
|
|
||
|
self.objectFetcher = nil;
|
||
|
self.properties = nil;
|
||
|
self.uploadProgressSelector = nil;
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
self.uploadProgressBlock = nil;
|
||
|
self.retryBlock = nil;
|
||
|
#endif
|
||
|
[self.executingQuery executionDidStop];
|
||
|
self.executingQuery = self.originalQuery;
|
||
|
|
||
|
[service_ autorelease];
|
||
|
service_ = nil;
|
||
|
}
|
||
|
|
||
|
- (id)service {
|
||
|
return service_;
|
||
|
}
|
||
|
|
||
|
- (void)setUserData:(id)userData {
|
||
|
[self setProperty:userData forKey:kUserDataPropertyKey];
|
||
|
}
|
||
|
|
||
|
- (id)userData {
|
||
|
// be sure the returned pointer has the life of the autorelease pool,
|
||
|
// in case self is released immediately
|
||
|
return [[[self propertyForKey:kUserDataPropertyKey] retain] autorelease];
|
||
|
}
|
||
|
|
||
|
- (void)setProperties:(NSDictionary *)dict {
|
||
|
[ticketProperties_ autorelease];
|
||
|
ticketProperties_ = [dict mutableCopy];
|
||
|
}
|
||
|
|
||
|
- (NSDictionary *)properties {
|
||
|
// be sure the returned pointer has the life of the autorelease pool,
|
||
|
// in case self is released immediately
|
||
|
return [[ticketProperties_ retain] autorelease];
|
||
|
}
|
||
|
|
||
|
- (void)setProperty:(id)obj forKey:(NSString *)key {
|
||
|
if (obj == nil) {
|
||
|
// user passed in nil, so delete the property
|
||
|
[ticketProperties_ removeObjectForKey:key];
|
||
|
} else {
|
||
|
// be sure the property dictionary exists
|
||
|
if (ticketProperties_ == nil) {
|
||
|
// call setProperties so observers are notified
|
||
|
[self setProperties:[NSDictionary dictionary]];
|
||
|
}
|
||
|
[ticketProperties_ setObject:obj forKey:key];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- (id)propertyForKey:(NSString *)key {
|
||
|
id obj = [ticketProperties_ objectForKey:key];
|
||
|
|
||
|
// be sure the returned pointer has the life of the autorelease pool,
|
||
|
// in case self is released immediately
|
||
|
return [[obj retain] autorelease];
|
||
|
}
|
||
|
|
||
|
- (NSDictionary *)surrogates {
|
||
|
return surrogates_;
|
||
|
}
|
||
|
|
||
|
- (void)setSurrogates:(NSDictionary *)dict {
|
||
|
[surrogates_ autorelease];
|
||
|
surrogates_ = [dict retain];
|
||
|
}
|
||
|
|
||
|
- (SEL)uploadProgressSelector {
|
||
|
return uploadProgressSelector_;
|
||
|
}
|
||
|
|
||
|
- (void)setUploadProgressSelector:(SEL)progressSelector {
|
||
|
uploadProgressSelector_ = progressSelector;
|
||
|
|
||
|
// if the user is turning on the progress selector in the ticket after the
|
||
|
// ticket's fetcher has been created, we need to give the fetcher our sentData
|
||
|
// callback.
|
||
|
//
|
||
|
// The progress monitor must be set in the service prior to creation of the
|
||
|
// ticket on 10.4 and iPhone 2.0, since on those systems the upload data must
|
||
|
// be wrapped with a ProgressMonitorInputStream prior to the creation of the
|
||
|
// fetcher.
|
||
|
if (progressSelector != NULL) {
|
||
|
SEL sentDataSel = @selector(objectFetcher:didSendBytes:totalBytesSent:totalBytesExpectedToSend:);
|
||
|
[[self objectFetcher] setSentDataSelector:sentDataSel];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#if NS_BLOCKS_AVAILABLE
|
||
|
- (void)setUploadProgressBlock:(GTLServiceUploadProgressBlock)block {
|
||
|
[uploadProgressBlock_ autorelease];
|
||
|
uploadProgressBlock_ = [block copy];
|
||
|
|
||
|
if (uploadProgressBlock_) {
|
||
|
// As above, we need the fetcher to call us back when bytes are sent.
|
||
|
SEL sentDataSel = @selector(objectFetcher:didSendBytes:totalBytesSent:totalBytesExpectedToSend:);
|
||
|
[[self objectFetcher] setSentDataSelector:sentDataSel];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- (GTLServiceUploadProgressBlock)uploadProgressBlock {
|
||
|
return uploadProgressBlock_;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
- (NSInteger)statusCode {
|
||
|
return [objectFetcher_ statusCode];
|
||
|
}
|
||
|
|
||
|
- (GTLQuery *)queryForRequestID:(NSString *)requestID {
|
||
|
id<GTLQueryProtocol> queryObj = self.executingQuery;
|
||
|
if ([queryObj isBatchQuery]) {
|
||
|
GTLBatchQuery *batch = (GTLBatchQuery *)queryObj;
|
||
|
GTLQuery *result = [batch queryForRequestID:requestID];
|
||
|
return result;
|
||
|
} else {
|
||
|
GTL_DEBUG_ASSERT(0, @"just use ticket.executingQuery");
|
||
|
return nil;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@end
|