diff --git a/Sources/Core/GTLRService.m b/Sources/Core/GTLRService.m index e55074d2e..d3eb1bec9 100644 --- a/Sources/Core/GTLRService.m +++ b/Sources/Core/GTLRService.m @@ -219,6 +219,7 @@ @implementation GTLRService { NSString *_overrideUserAgent; NSDictionary *_serviceProperties; // Properties retained for the convenience of the client app. NSUInteger _uploadChunkSize; // Only applies to resumable chunked uploads. + dispatch_queue_t _requestCreationQueue; } @synthesize additionalHTTPHeaders = _additionalHTTPHeaders, @@ -250,6 +251,9 @@ - (instancetype)init { if (self) { _parseQueue = dispatch_queue_create("com.google.GTLRServiceParse", DISPATCH_QUEUE_SERIAL); _callbackQueue = dispatch_get_main_queue(); + _requestCreationQueue = + dispatch_queue_create("com.google.GTLRServiceRequestCreation", DISPATCH_QUEUE_SERIAL); + _fetcherService = [[GTMSessionFetcherService alloc] init]; // Make the session fetcher use a background delegate queue instead of bouncing @@ -319,10 +323,26 @@ - (void)setMainBundleIDRestrictionWithAPIKey:(NSString *)apiKey { self.APIKeyRestrictionBundleID = [[NSBundle mainBundle] bundleIdentifier]; } -- (NSMutableURLRequest *)requestForURL:(NSURL *)url - ETag:(NSString *)etag - httpMethod:(NSString *)httpMethod - ticket:(GTLRServiceTicket *)ticket { +- (void)requestForURL:(NSURL *)url + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + ticket:(GTLRServiceTicket *)ticket + completion:(void (^)(NSMutableURLRequest *))completion { + dispatch_async(_requestCreationQueue, ^{ + NSMutableURLRequest *request = [self createRequestForURL:url + ETag:etag + httpMethod:httpMethod + ticket:ticket]; + completion(request); + }); +} + +- (NSMutableURLRequest *)createRequestForURL:(NSURL *)url + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + ticket:(GTLRServiceTicket *)ticket { + // This method may block, so make sure it's not on the caller's queue when executing a query. + dispatch_assert_queue_debug(_requestCreationQueue); // subclasses may add headers to this NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url @@ -371,19 +391,20 @@ - (NSMutableURLRequest *)requestForURL:(NSURL *)url return request; } -// objectRequestForURL returns an NSMutableURLRequest for a GTLRObject +// objectRequestForURL asynchronously returns an NSMutableURLRequest for a GTLRObject // // 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:(GTLRObject *)object - contentType:(NSString *)contentType - contentLength:(NSString *)contentLength - ETag:(NSString *)etag - httpMethod:(NSString *)httpMethod - additionalHeaders:(NSDictionary *)additionalHeaders - ticket:(GTLRServiceTicket *)ticket { +- (void)objectRequestForURL:(NSURL *)url + object:(GTLRObject *)object + contentType:(NSString *)contentType + contentLength:(NSString *)contentLength + ETag:(NSString *)etag + httpMethod:(NSString *)httpMethod + additionalHeaders:(NSDictionary *)additionalHeaders + ticket:(GTLRServiceTicket *)ticket + completion:(void (^)(NSMutableURLRequest *))completion { 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 @@ -396,10 +417,23 @@ - (NSMutableURLRequest *)objectRequestForURL:(NSURL *)url } } - NSMutableURLRequest *request = [self requestForURL:url - ETag:etag - httpMethod:httpMethod - ticket:ticket]; + [self requestForURL:url + ETag:etag + httpMethod:httpMethod + ticket:ticket + completion:^(NSMutableURLRequest *request) { + [self handleRequestCompletion:request + contentType:contentType + contentLength:contentLength + additionalHeaders:additionalHeaders]; + completion(request); + }]; +} + +- (void)handleRequestCompletion:(NSMutableURLRequest *)request + contentType:(NSString *)contentType + contentLength:(NSString *)contentLength + additionalHeaders:(NSDictionary *)additionalHeaders { [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; [request setValue:contentType forHTTPHeaderField:@"Content-Type"]; @@ -421,13 +455,25 @@ - (NSMutableURLRequest *)objectRequestForURL:(NSURL *)url NSString *value = [headers objectForKey:key]; [request setValue:value forHTTPHeaderField:key]; } - - return request; } #pragma mark - - (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query { + dispatch_semaphore_t requestCompleteSemaphore = dispatch_semaphore_create(0); + + __block NSMutableURLRequest *result; + [self requestForQuery:query + completion:^(NSMutableURLRequest *request) { + result = request; + dispatch_semaphore_signal(requestCompleteSemaphore); + }]; + + dispatch_semaphore_wait(requestCompleteSemaphore, DISPATCH_TIME_FOREVER); + return result; +} + +- (void)requestForQuery:(GTLRQuery *)query completion:(void (^)(NSMutableURLRequest *))completion { GTLR_DEBUG_ASSERT(query.bodyObject == nil, @"requestForQuery: supports only GET methods, but was passed: %@", query); GTLR_DEBUG_ASSERT(query.uploadParameters == nil, @@ -446,10 +492,17 @@ - (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query { queryParameters:queryParameters]; } - NSMutableURLRequest *request = [self requestForURL:url - ETag:nil - httpMethod:query.httpMethod - ticket:nil]; + [self requestForURL:url + ETag:nil + httpMethod:query.httpMethod + ticket:nil + completion:^(NSMutableURLRequest *request) { + [self handleRequestCompletion:request forQuery:query]; + completion(request); + }]; +} + +- (void)handleRequestCompletion:(NSMutableURLRequest *)request forQuery:(GTLRQuery *)query { NSString *apiRestriction = self.APIKeyRestrictionBundleID; if ([apiRestriction length] > 0) { [request setValue:apiRestriction forHTTPHeaderField:kXIosBundleIdHeader]; @@ -466,8 +519,6 @@ - (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query { NSString *value = [headers objectForKey:key]; [request setValue:value forHTTPHeaderField:key]; } - - return request; } // common fetch starting method @@ -574,14 +625,6 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL } } - NSURLRequest *request = [self objectRequestForURL:targetURL - object:bodyObject - contentType:contentType - contentLength:contentLength - ETag:etag - httpMethod:httpMethod - additionalHeaders:additionalHeaders - ticket:ticket]; ticket.postedObject = bodyObject; ticket.executingQuery = executingQuery; @@ -591,10 +634,39 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL ticket.originalQuery = originalQuery; } + [self objectRequestForURL:targetURL + object:bodyObject + contentType:contentType + contentLength:contentLength + ETag:etag + httpMethod:httpMethod + additionalHeaders:additionalHeaders + ticket:ticket + completion:^(NSMutableURLRequest *request) { + [self handleObjectRequestCompletionWithRequest:request + objectClass:objectClass + dataToPost:dataToPost + mayAuthorize:mayAuthorize + completionHandler:completionHandler + executingQuery:executingQuery + ticket:ticket]; + }]; + + return ticket; +} + +- (void)handleObjectRequestCompletionWithRequest:(NSMutableURLRequest *)request + objectClass:(Class)objectClass + dataToPost:(NSData *)dataToPost + mayAuthorize:(BOOL)mayAuthorize + completionHandler:(GTLRServiceCompletionHandler)completionHandler + executingQuery:(id)executingQuery + ticket:(GTLRServiceTicket *)ticket { // Some proxy servers (and some web servers) have issues with GET URLs being // too long, trap that and move the query parameters into the body. The // uploadParams and dataToPost should be nil for a GET, but playing it safe // and confirming. + GTLRUploadParameters *uploadParams = executingQuery.uploadParameters; NSString *requestHTTPMethod = request.HTTPMethod; BOOL isDoingHTTPGet = (requestHTTPMethod == nil @@ -630,7 +702,7 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL testBlock:testBlock dataToPost:dataToPost completionHandler:completionHandler]; - return ticket; + return; } GTMSessionFetcherService *fetcherService = ticket.fetcherService; @@ -814,24 +886,13 @@ - (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL } [self handleParsedObjectForFetcher:fetcher - executingQuery:executingQuery - ticket:ticket - error:error - parsedObject:nil - hasSentParsingStartNotification:NO - completionHandler:completionHandler]; + executingQuery:executingQuery + ticket:ticket + error:error + parsedObject:nil + hasSentParsingStartNotification:NO + completionHandler:completionHandler]; }]; // fetcher completion handler - - // 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 (ticket.hasCalledCallback) { - return nil; - } - - return ticket; } - (GTMSessionUploadFetcher *)uploadFetcherWithRequest:(NSURLRequest *)request diff --git a/Sources/Core/Public/GoogleAPIClientForREST/GTLRService.h b/Sources/Core/Public/GoogleAPIClientForREST/GTLRService.h index 7b0f30027..481ccf35a 100644 --- a/Sources/Core/Public/GoogleAPIClientForREST/GTLRService.h +++ b/Sources/Core/Public/GoogleAPIClientForREST/GTLRService.h @@ -384,7 +384,29 @@ typedef void (^GTLRServiceTestBlock)(GTLRServiceTicket *testTicket, * * @return A request suitable for use with @c GTMSessionFetcher or @c NSURLSession */ -- (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query; +- (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query + __attribute__((deprecated("Blocks the caller to calculate the User-Agent. Use " + "-requestForQuery:completion: instead."))); + +/** + * Creates a NSURLRequest from the query object and from properties on this service + * (additionalHTTPHeaders, additionalURLQueryParameters, APIKey) without executing + * it. This can be useful for using @c GTMSessionFetcher or @c NSURLSession to + * perform the fetch. + * + * For requests to non-public resources, the request will not yet be authorized; + * that can be done using the GTLR service's authorizer. Creating a @c GTMSessionFetcher + * from the GTLRService's @c fetcherService will take care of authorization as well. + * + * This works only for GET queries, and only for an individual query, not a batch query. + * + * @note @c Unlike executeQuery:, requestForQuery: does not release the query's callback blocks. + * + * @param query The query used to create the request. + * @param completion Completion invoked with the URL request suitable for use + * with with @c GTMSessionFetcher or @c NSURLSession + */ +- (void)requestForQuery:(GTLRQuery *)query completion:(void (^)(NSMutableURLRequest *))completion; #pragma mark User Properties