GBA002/Pods/GoogleAPIClientForREST/Source/Objects/GTLRService.m
Riley Testut 6cca0f244f Replaces frameworks with static libraries
As of iOS 13.3.1, apps installed with free developer accounts that contain embedded frameworks fail to launch. To work around this, we now link all dependencies via Cocoapods as static libraries.
2020-02-03 19:28:23 -08:00

2887 lines
114 KiB
Objective-C

/* 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.
*/
#if !__has_feature(objc_arc)
#error "This file needs to be compiled with ARC enabled."
#endif
#import <TargetConditionals.h>
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#endif
#if !defined(GTLR_USE_FRAMEWORK_IMPORTS)
#if defined(COCOAPODS) && COCOAPODS
#define GTLR_USE_FRAMEWORK_IMPORTS 1
#else
#define GTLR_USE_FRAMEWORK_IMPORTS 0
#endif
#endif
#import "GTLRService.h"
#import "GTLRFramework.h"
#import "GTLRURITemplate.h"
#import "GTLRUtilities.h"
#if GTLR_USE_FRAMEWORK_IMPORTS
#import <GTMSessionFetcher/GTMSessionFetcher.h>
#import <GTMSessionFetcher/GTMSessionFetcherService.h>
#import <GTMSessionFetcher/GTMMIMEDocument.h>
#else
#import "GTMSessionFetcher.h"
#import "GTMSessionFetcherService.h"
#import "GTMMIMEDocument.h"
#endif // GTLR_USE_FRAMEWORK_IMPORTS
#ifndef STRIP_GTM_FETCH_LOGGING
#error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
#endif
NSString *const kGTLRServiceErrorDomain = @"com.google.GTLRServiceDomain";
NSString *const kGTLRErrorObjectDomain = @"com.google.GTLRErrorObjectDomain";
NSString *const kGTLRServiceErrorBodyDataKey = @"body";
NSString *const kGTLRServiceErrorContentIDKey = @"contentID";
NSString *const kGTLRStructuredErrorKey = @"GTLRStructuredError";
NSString *const kGTLRETagWildcard = @"*";
NSString *const kGTLRServiceTicketStartedNotification = @"kGTLRServiceTicketStartedNotification";
NSString *const kGTLRServiceTicketStoppedNotification = @"kGTLRServiceTicketStoppedNotification";
NSString *const kGTLRServiceTicketParsingStartedNotification = @"kGTLRServiceTicketParsingStartedNotification";
NSString *const kGTLRServiceTicketParsingStoppedNotification = @"kGTLRServiceTicketParsingStoppedNotification";
NSString *const kXIosBundleIdHeader = @"X-Ios-Bundle-Identifier";
static NSString *const kDeveloperAPIQueryParamKey = @"key";
static const NSUInteger kMaxNumberOfNextPagesFetched = 25;
static const NSUInteger kMaxGETURLLength = 2048;
// we'll enforce 50K chunks minimum just to avoid the server getting hit
// with too many small upload chunks
static const NSUInteger kMinimumUploadChunkSize = 50000;
// Helper to get the ETag if it is defined on an object.
static NSString *ETagIfPresent(GTLRObject *obj) {
NSString *result = [obj.JSON objectForKey:@"etag"];
return result;
}
// Merge two dictionaries. Either may be nil.
// If both are nil, return nil.
// In case of a key collision, values of the second dictionary prevail.
static NSDictionary *MergeDictionaries(NSDictionary *recessiveDict, NSDictionary *dominantDict) {
if (!dominantDict) return recessiveDict;
if (!recessiveDict) return dominantDict;
NSMutableDictionary *worker = [recessiveDict mutableCopy];
[worker addEntriesFromDictionary:dominantDict];
return worker;
}
@interface GTLRServiceTicket ()
- (instancetype)initWithService:(GTLRService *)service
executionParameters:(GTLRServiceExecutionParameters *)params NS_DESIGNATED_INITIALIZER;
// Thread safety: ticket properties are all publicly exposed as read-only.
//
// Service execution of a ticket is serial (started by the app, then executing on the fetcher
// callback queue and then the parse queue), so we don't need to worry about synchronization.
//
// One important exception is when the user invoked cancelTicket. During cancellation, ticket
// properties are released. This should be harmless even during the fetch start-parse-callback
// phase because nothing released in cancelTicket is used to begin a fetch, and the cancellation
// flag will prevent any application callbacks from being invoked.
//
// The cancel and objectFetcher properties are synchronized on the ticket.
// Ticket properties exposed publicly as readonly.
@property(atomic, readwrite, nullable) id<GTLRQueryProtocol> originalQuery;
@property(atomic, readwrite, nullable) id<GTLRQueryProtocol> executingQuery;
@property(atomic, readwrite, nullable) GTMSessionFetcher *objectFetcher;
@property(nonatomic, readwrite, nullable) NSURLRequest *fetchRequest;
@property(nonatomic, readwrite, nullable) GTLRObject *postedObject;
@property(nonatomic, readwrite, nullable) GTLRObject *fetchedObject;
@property(nonatomic, readwrite, nullable) NSError *fetchError;
@property(nonatomic, readwrite) BOOL hasCalledCallback;
@property(nonatomic, readwrite) NSUInteger pagesFetchedCounter;
@property(readwrite, atomic, strong) id<GTLRObjectClassResolver> objectClassResolver;
// Internal properties copied from the service.
@property(nonatomic, assign) BOOL allowInsecureQueries;
@property(nonatomic, strong) GTMSessionFetcherService *fetcherService;
@property(nonatomic, strong, nullable) id<GTMFetcherAuthorizationProtocol> authorizer;
// Internal properties copied from serviceExecutionParameters.
@property(nonatomic, getter=isRetryEnabled) BOOL retryEnabled;
@property(nonatomic, readwrite) NSTimeInterval maxRetryInterval;
@property(nonatomic, strong, nullable) GTLRServiceRetryBlock retryBlock;
@property(nonatomic, strong, nullable) GTLRServiceUploadProgressBlock uploadProgressBlock;
@property(nonatomic, strong, nullable) GTLRServiceTestBlock testBlock;
@property(nonatomic, readwrite) BOOL shouldFetchNextPages;
// Internal properties used by the service.
#if GTM_BACKGROUND_TASK_FETCHING
// Access to backgroundTaskIdentifier should be protected by @synchronized(self).
@property(nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
#endif // GTM_BACKGROUND_TASK_FETCHING
// Dispatch group enabling waitForTicket: to delay until async callbacks and notifications
// related to the ticket have completed.
@property(nonatomic, readonly) dispatch_group_t callbackGroup;
// startBackgroundTask and endBackgroundTask do nothing if !GTM_BACKGROUND_TASK_FETCHING
- (void)startBackgroundTask;
- (void)endBackgroundTask;
- (void)notifyStarting:(BOOL)isStarting;
- (void)releaseTicketCallbacks;
// Posts a notification on the main queue using the ticket's dispatch group.
- (void)postNotificationOnMainThreadWithName:(NSString *)name
object:(id)object
userInfo:(NSDictionary *)userInfo;
@end
#if !defined(GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT)
#if defined(COCOAPODS) && COCOAPODS
#define GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT 1
#else
#define GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT 0
#endif
#endif
#if GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT
#if GTLR_USE_FRAMEWORK_IMPORTS
#import <GTMSessionFetcher/GTMSessionUploadFetcher.h>
#else
#import "GTMSessionUploadFetcher.h"
#endif // GTLR_USE_FRAMEWORK_IMPORTS
#else
// If the upload fetcher class is available, it can be used for chunked uploads
//
// We locally declare some methods of the upload fetcher so we
// do not need to import the header, as some projects may not have it available
@interface GTMSessionUploadFetcher : GTMSessionFetcher
+ (instancetype)uploadFetcherWithRequest:(NSURLRequest *)request
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil;
+ (instancetype)uploadFetcherWithLocation:(NSURL *)uploadLocationURL
uploadMIMEType:(NSString *)uploadMIMEType
chunkSize:(int64_t)chunkSize
fetcherService:(GTM_NULLABLE GTMSessionFetcherService *)fetcherServiceOrNil;
@property(strong) NSURL *uploadLocationURL;
@property(strong) NSData *uploadData;
@property(strong) NSURL *uploadFileURL;
@property(strong) NSFileHandle *uploadFileHandle;
- (void)pauseFetching;
- (void)resumeFetching;
- (BOOL)isPaused;
@end
#endif // GTLR_HAS_SESSION_UPLOAD_FETCHER_IMPORT
@interface GTLRObject (StandardProperties)
// Common properties on GTLRObject that are invoked below.
@property(nonatomic, copy) NSString *nextPageToken;
@end
// This class encapsulates the pieces of a single batch response, including
// inner http response code and message, inner headers, JSON body (parsed as a dictionary),
// or parsing NSError.
//
// See responsePartsWithMIMEParts: for an example of the wire format data used
// to populate this object.
@interface GTLRBatchResponsePart : NSObject
@property(nonatomic, copy) NSString *contentID;
@property(nonatomic, assign) NSInteger statusCode;
@property(nonatomic, copy) NSString *statusString;
@property(nonatomic, strong) NSDictionary *headers;
@property(nonatomic, strong) NSDictionary *JSON;
@property(nonatomic, strong) NSError *parseError;
@end
@implementation GTLRBatchResponsePart
@synthesize contentID = _contentID,
headers = _headers,
JSON = _JSON,
parseError = _parseError,
statusCode = _statusCode,
statusString = _statusString;
#if DEBUG
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %p: %@\n%ld %@\nheaders:%@\nJSON:%@\nerror:%@",
[self class], self, self.contentID, (long)self.statusCode, self.statusString,
self.headers, self.JSON, self.parseError];
}
#endif
@end
// GTLRResourceURLQuery is an internal class used as a query object placeholder
// when fetchObjectWithURL: is invoked by the client app. This lets the service's
// plumbing treat the request like other queries, without allowing users to
// set arbitrary query properties that may not work as anticipated.
@interface GTLRResourceURLQuery : GTLRQuery
@property(nonatomic, strong, nullable) NSURL *resourceURL;
+ (instancetype)queryWithResourceURL:(NSURL *)resourceURL
objectClass:(nullable Class)objectClass;
@end
@implementation GTLRService {
NSString *_userAgent;
NSString *_overrideUserAgent;
NSDictionary *_serviceProperties; // Properties retained for the convenience of the client app.
NSUInteger _uploadChunkSize; // Only applies to resumable chunked uploads.
}
@synthesize additionalHTTPHeaders = _additionalHTTPHeaders,
additionalURLQueryParameters = _additionalURLQueryParameters,
allowInsecureQueries = _allowInsecureQueries,
callbackQueue = _callbackQueue,
APIKey = _apiKey,
APIKeyRestrictionBundleID = _apiKeyRestrictionBundleID,
batchPath = _batchPath,
dataWrapperRequired = _dataWrapperRequired,
fetcherService = _fetcherService,
maxRetryInterval = _maxRetryInterval,
parseQueue = _parseQueue,
prettyPrintQueryParameterNames = _prettyPrintQueryParameterNames,
resumableUploadPath = _resumableUploadPath,
retryBlock = _retryBlock,
retryEnabled = _retryEnabled,
rootURLString = _rootURLString,
servicePath = _servicePath,
shouldFetchNextPages = _shouldFetchNextPages,
simpleUploadPath = _simpleUploadPath,
objectClassResolver = _objectClassResolver,
testBlock = _testBlock,
uploadProgressBlock = _uploadProgressBlock,
userAgentAddition = _userAgentAddition;
+ (Class)ticketClass {
return [GTLRServiceTicket class];
}
- (instancetype)init {
self = [super init];
if (self) {
_parseQueue = dispatch_queue_create("com.google.GTLRServiceParse", DISPATCH_QUEUE_SERIAL);
_callbackQueue = dispatch_get_main_queue();
_fetcherService = [[GTMSessionFetcherService alloc] init];
// Make the session fetcher use a background delegate queue instead of bouncing
// through the main queue for its callbacks from NSURLSession. This should improve
// performance, and eventually be the default behavior for the fetcher.
NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init];
delegateQueue.maxConcurrentOperationCount = 1;
delegateQueue.name = @"com.google.GTLRServiceFetcherDelegate";
_fetcherService.sessionDelegateQueue = delegateQueue;
NSDictionary<NSString *, Class> *kindMap = [[self class] kindStringToClassMap];
_objectClassResolver = [GTLRObjectClassResolver resolverWithKindMap:kindMap];
}
return self;
}
- (NSString *)requestUserAgent {
if (_overrideUserAgent != nil) {
return _overrideUserAgent;
}
NSString *userAgent = self.userAgent;
if (userAgent.length == 0) {
// The service instance is missing an explicit user-agent; use the bundle ID
// or process name. Don't use the bundle ID of the library's framework.
NSBundle *owningBundle = [NSBundle bundleForClass:[self class]];
if (owningBundle == nil
|| [owningBundle.bundleIdentifier isEqual:@"com.google.GTLR"]) {
owningBundle = [NSBundle mainBundle];
}
userAgent = GTMFetcherApplicationIdentifier(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 = GTLRFrameworkVersionString();
NSString *systemString = GTMFetcherSystemVersionString();
// 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;
}
- (void)setMainBundleIDRestrictionWithAPIKey:(NSString *)apiKey {
self.APIKey = apiKey;
self.APIKeyRestrictionBundleID = [[NSBundle mainBundle] bundleIdentifier];
}
- (NSMutableURLRequest *)requestForURL:(NSURL *)url
ETag:(NSString *)etag
httpMethod:(NSString *)httpMethod
ticket:(GTLRServiceTicket *)ticket {
// subclasses may add headers to this
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
cachePolicy:NSURLRequestReloadIgnoringCacheData
timeoutInterval:60];
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;
}
// objectRequestForURL 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 {
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) {
etag = ETagIfPresent(object);
}
}
NSMutableURLRequest *request = [self requestForURL:url
ETag:etag
httpMethod:httpMethod
ticket:ticket];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
[request setValue:contentType forHTTPHeaderField:@"Content-Type"];
[request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
if (contentLength) {
[request setValue:contentLength forHTTPHeaderField:@"Content-Length"];
}
// 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 objectForKey:key];
[request setValue:value forHTTPHeaderField:key];
}
headers = additionalHeaders;
for (NSString *key in headers) {
NSString *value = [headers objectForKey:key];
[request setValue:value forHTTPHeaderField:key];
}
return request;
}
#pragma mark -
- (NSMutableURLRequest *)requestForQuery:(GTLRQuery *)query {
GTLR_DEBUG_ASSERT(query.bodyObject == nil,
@"requestForQuery: supports only GET methods, but was passed: %@", query);
GTLR_DEBUG_ASSERT(query.uploadParameters == nil,
@"requestForQuery: does not support uploads, but was passed: %@", query);
NSURL *url = [self URLFromQueryObject:query
usePartialPaths:NO
includeServiceURLQueryParams:YES];
// If there is a developer key, add it onto the url.
NSString *apiKey = self.APIKey;
if (apiKey.length > 0) {
NSDictionary *queryParameters;
queryParameters = @{ kDeveloperAPIQueryParamKey : apiKey };
url = [GTLRService URLWithString:url.absoluteString
queryParameters:queryParameters];
}
NSMutableURLRequest *request = [self requestForURL:url
ETag:nil
httpMethod:query.httpMethod
ticket:nil];
NSString *apiRestriction = self.APIKeyRestrictionBundleID;
if ([apiRestriction length] > 0) {
[request setValue:apiRestriction forHTTPHeaderField:kXIosBundleIdHeader];
}
NSDictionary *headers = self.additionalHTTPHeaders;
for (NSString *key in headers) {
NSString *value = [headers objectForKey:key];
[request setValue:value forHTTPHeaderField:key];
}
headers = query.additionalHTTPHeaders;
for (NSString *key in headers) {
NSString *value = [headers objectForKey:key];
[request setValue:value forHTTPHeaderField:key];
}
return request;
}
// common fetch starting method
- (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL
objectClass:(Class)objectClass
bodyObject:(GTLRObject *)bodyObject
dataToPost:(NSData *)dataToPost
ETag:(NSString *)etag
httpMethod:(NSString *)httpMethod
mayAuthorize:(BOOL)mayAuthorize
completionHandler:(GTLRServiceCompletionHandler)completionHandler
executingQuery:(id<GTLRQueryProtocol>)executingQuery
ticket:(GTLRServiceTicket *)ticket {
// Once inside this method, we should not access any service properties that may reasonably
// be changed by the app, as this method may execute multiple times during query execution
// and we want consistent behavior. Service properties should be copied to the ticket.
GTLR_DEBUG_ASSERT(executingQuery != nil,
@"no query? service additionalURLQueryParameters needs to be added to targetURL");
GTLR_DEBUG_ASSERT(targetURL != nil, @"no url?");
if (targetURL == nil) return nil;
BOOL hasExecutionParams = [executingQuery hasExecutionParameters];
GTLRServiceExecutionParameters *executionParams = (hasExecutionParams ?
executingQuery.executionParameters : nil);
// We need to create a ticket unless one was created earlier (like during authentication.)
if (!ticket) {
ticket = [[[[self class] ticketClass] alloc] initWithService:self
executionParameters:executionParams];
[ticket notifyStarting:YES];
}
// If there is a developer key, add it onto the URL.
NSString *apiKey = ticket.APIKey;
if (apiKey.length > 0) {
NSDictionary *queryParameters;
queryParameters = @{ kDeveloperAPIQueryParamKey : apiKey };
targetURL = [GTLRService URLWithString:targetURL.absoluteString
queryParameters:queryParameters];
}
NSString *contentType = @"application/json; charset=utf-8";
NSString *contentLength; // nil except for single-request uploads.
if ([executingQuery isBatchQuery]) {
contentType = [NSString stringWithFormat:@"multipart/mixed; boundary=%@",
((GTLRBatchQuery *)executingQuery).boundary];
}
GTLRUploadParameters *uploadParams = executingQuery.uploadParameters;
if (uploadParams.shouldUploadWithSingleRequest) {
NSData *uploadData = uploadParams.data;
NSString *uploadMIMEType = uploadParams.MIMEType;
if (!uploadData) {
GTLR_DEBUG_ASSERT(0, @"Uploading with a single request requires bytes to upload as NSData");
} else {
if (uploadParams.shouldSendUploadOnly) {
contentType = uploadMIMEType;
dataToPost = uploadData;
contentLength = @(dataToPost.length).stringValue;
} else {
GTMMIMEDocument *mimeDoc = [GTMMIMEDocument MIMEDocument];
if (dataToPost) {
// Include the object as metadata with the upload.
[mimeDoc addPartWithHeaders:@{ @"Content-Type" : contentType }
body:dataToPost];
}
[mimeDoc addPartWithHeaders:@{ @"Content-Type" : uploadMIMEType }
body:uploadData];
dispatch_data_t mimeDispatchData;
unsigned long long mimeLength;
NSString *mimeBoundary;
[mimeDoc generateDispatchData:&mimeDispatchData
length:&mimeLength
boundary:&mimeBoundary];
contentType = [NSString stringWithFormat:@"multipart/related; boundary=%@", mimeBoundary];
dataToPost = (NSData *)mimeDispatchData;
contentLength = @(mimeLength).stringValue;
}
}
}
NSDictionary *additionalHeaders = nil;
NSString *restriction = self.APIKeyRestrictionBundleID;
if ([restriction length] > 0) {
additionalHeaders = @{ kXIosBundleIdHeader : restriction };
}
NSDictionary *queryAdditionalHeaders = executingQuery.additionalHTTPHeaders;
if (queryAdditionalHeaders) {
if (additionalHeaders) {
NSMutableDictionary *builder = [additionalHeaders mutableCopy];
[builder addEntriesFromDictionary:queryAdditionalHeaders];
additionalHeaders = builder;
} else {
additionalHeaders = queryAdditionalHeaders;
}
}
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;
GTLRQuery *originalQuery = (GTLRQuery *)ticket.originalQuery;
if (originalQuery == nil) {
originalQuery = (GTLRQuery *)executingQuery;
ticket.originalQuery = originalQuery;
}
// 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.
NSString *requestHTTPMethod = request.HTTPMethod;
BOOL isDoingHTTPGet =
(requestHTTPMethod == nil
|| [requestHTTPMethod caseInsensitiveCompare:@"GET"] == NSOrderedSame);
if (isDoingHTTPGet &&
(request.URL.absoluteString.length >= kMaxGETURLLength) &&
(uploadParams == nil) &&
(dataToPost == nil)) {
NSString *urlString = request.URL.absoluteString;
NSRange range = [urlString rangeOfString:@"?"];
if (range.location != NSNotFound) {
NSURL *trimmedURL = [NSURL URLWithString:[urlString substringToIndex:range.location]];
NSString *urlArgsString = [urlString substringFromIndex:(range.location + 1)];
if (trimmedURL && (urlArgsString.length > 0)) {
dataToPost = [urlArgsString dataUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *mutableRequest = [request mutableCopy];
mutableRequest.URL = trimmedURL;
mutableRequest.HTTPMethod = @"POST";
[mutableRequest setValue:@"GET" forHTTPHeaderField:@"X-HTTP-Method-Override"];
[mutableRequest setValue:@"application/x-www-form-urlencoded"
forHTTPHeaderField:@"Content-Type"];
[mutableRequest setValue:@(dataToPost.length).stringValue
forHTTPHeaderField:@"Content-Length"];
request = mutableRequest;
}
}
}
ticket.fetchRequest = request;
GTLRServiceTestBlock testBlock = ticket.testBlock;
if (testBlock) {
[self simulateFetchWithTicket:ticket
testBlock:testBlock
dataToPost:dataToPost
completionHandler:completionHandler];
return ticket;
}
GTMSessionFetcherService *fetcherService = ticket.fetcherService;
GTMSessionFetcher *fetcher;
if (uploadParams == nil || uploadParams.shouldUploadWithSingleRequest) {
// Create a single-request fetcher.
fetcher = [fetcherService fetcherWithRequest:request];
} else {
fetcher = [self uploadFetcherWithRequest:request
fetcherService:fetcherService
params:uploadParams];
}
if (ticket.allowInsecureQueries) {
fetcher.allowLocalhostRequest = YES;
fetcher.allowedInsecureSchemes = @[ @"http" ];
}
NSString *loggingName = executingQuery.loggingName;
if (loggingName.length > 0) {
NSUInteger pageNumber = ticket.pagesFetchedCounter + 1;
if (pageNumber > 1) {
loggingName = [loggingName stringByAppendingFormat:@", page %lu",
(unsigned long)pageNumber];
}
fetcher.comment = loggingName;
}
if (!mayAuthorize) {
fetcher.authorizer = nil;
} else {
fetcher.authorizer = ticket.authorizer;
}
// copy the ticket's retry settings into the fetcher
fetcher.retryEnabled = ticket.retryEnabled;
fetcher.maxRetryInterval = ticket.maxRetryInterval;
BOOL shouldExamineRetries = (ticket.retryBlock != nil);
if (shouldExamineRetries) {
GTLR_DEBUG_ASSERT(ticket.retryEnabled, @"Setting retry block without retry enabled.");
fetcher.retryBlock = ^(BOOL suggestedWillRetry, NSError *error,
GTMSessionFetcherRetryResponse response) {
// The object fetcher may call into this retry block; this one invokes the
// selector provided by the user.
GTLRServiceRetryBlock retryBlock = ticket.retryBlock;
if (!retryBlock) {
response(suggestedWillRetry);
} else {
dispatch_group_async(ticket.callbackGroup, ticket.callbackQueue, ^{
if (ticket.cancelled) {
response(NO);
return;
}
BOOL willRetry = retryBlock(ticket, suggestedWillRetry, error);
response(willRetry);
});
}
};
}
// Remember the object fetcher in the ticket.
ticket.objectFetcher = fetcher;
// Set the upload data.
fetcher.bodyData = dataToPost;
// Have the fetcher call back on the parse queue.
fetcher.callbackQueue = self.parseQueue;
// If this ticket is paging, end any ongoing background task immediately, and
// rely on the fetcher's background task now instead.
[ticket endBackgroundTask];
[fetcher beginFetchWithCompletionHandler:^(NSData * _Nullable data, NSError * _Nullable error) {
// We now have the JSON data for an object, or an error.
GTLR_ASSERT_CURRENT_QUEUE_DEBUG(self.parseQueue);
// Until now, the only async operation has been the fetch, and we rely on the fetcher's
// background task on iOS to get us here if the app was backgrounded.
//
// Now we'll let the ticket create a background task so that the async parsing and call back to
// the app will happen if the app is sent to the background. The ticket is responsible for
// ending the background task.
[ticket startBackgroundTask];
if (ticket.cancelled) {
// If the user cancels the ticket, then cancelTicket will stop the fetcher so this
// callback probably won't occur.
//
// But just for safety, if we get here, skip any parsing steps by fabricating an error.
data = nil;
error = [NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorCancelled
userInfo:nil];
}
if (error == nil) {
// Successful fetch.
if (data.length > 0) {
[self prepareToParseObjectForFetcher:fetcher
executingQuery:executingQuery
ticket:ticket
error:error
defaultClass:objectClass
completionHandler:completionHandler];
} else {
// no data (such as when deleting)
[self handleParsedObjectForFetcher:fetcher
executingQuery:executingQuery
ticket:ticket
error:nil
parsedObject:nil
hasSentParsingStartNotification:NO
completionHandler:completionHandler];
}
return;
}
// Failed 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 *responseContentType = [responseHeaders objectForKey:@"Content-Type"];
if (data.length > 0) {
if ([responseContentType hasPrefix:@"application/json"]) {
NSError *parseError = nil;
NSMutableDictionary *jsonWrapper =
[NSJSONSerialization JSONObjectWithData:(NSData * _Nonnull)data
options:NSJSONReadingMutableContainers
error:&parseError];
// If the json parse worked, then extract potentially better
// information.
if (!parseError) {
// HTTP Streaming defined by Google services is is an array
// of requests and replies. This code never makes one of
// these requests; but, some GET apis can actually be to
// a Streaming result (for media?), so the errors can still
// come back in an array.
if ([jsonWrapper isKindOfClass:[NSArray class]]) {
NSArray *jsonWrapperAsArray = (NSArray *)jsonWrapper;
#if DEBUG
if (jsonWrapperAsArray.count > 1) {
GTLR_DEBUG_LOG(@"Got error array with >1 item, only using first. Full list: %@",
jsonWrapperAsArray);
}
#endif
// Use the first.
jsonWrapper = [jsonWrapperAsArray firstObject];
}
// Convert the JSON error payload into a structured error
NSMutableDictionary *errorJSON = [jsonWrapper valueForKey:@"error"];
if (errorJSON) {
GTLRErrorObject *errorObject = [GTLRErrorObject 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:(NSData * _Nonnull)data
encoding:NSUTF8StringEncoding];
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : reasonStr };
error = [NSError errorWithDomain:kGTMSessionFetcherStatusDomain
code:status
userInfo:userInfo];
}
} else {
// Response data length is zero; we'll settle for returning the
// fetcher's error.
}
}
}
[self handleParsedObjectForFetcher:fetcher
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
fetcherService:(GTMSessionFetcherService *)fetcherService
params:(GTLRUploadParameters *)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;
}
NSString *uploadClassName = GTLR_CLASSNAME_STR(GTMSessionUploadFetcher);
Class uploadClass = NSClassFromString(uploadClassName);
GTLR_ASSERT(uploadClass != nil, @"GTMSessionUploadFetcher needed");
NSString *uploadMIMEType = uploadParams.MIMEType;
NSData *uploadData = uploadParams.data;
NSURL *uploadFileURL = uploadParams.fileURL;
NSFileHandle *uploadFileHandle = uploadParams.fileHandle;
NSURL *uploadLocationURL = uploadParams.uploadLocationURL;
// Create the upload fetcher.
GTMSessionUploadFetcher *fetcher;
if (uploadLocationURL) {
// Resuming with the session fetcher and a file URL.
GTLR_DEBUG_ASSERT(uploadFileURL != nil, @"Resume requires a file URL");
fetcher = [uploadClass uploadFetcherWithLocation:uploadLocationURL
uploadMIMEType:uploadMIMEType
chunkSize:(int64_t)uploadChunkSize
fetcherService:fetcherService];
fetcher.uploadFileURL = uploadFileURL;
} else {
fetcher = [uploadClass uploadFetcherWithRequest:request
uploadMIMEType:uploadMIMEType
chunkSize:(int64_t)uploadChunkSize
fetcherService:fetcherService];
if (uploadFileURL) {
fetcher.uploadFileURL = uploadFileURL;
} else if (uploadData) {
fetcher.uploadData = uploadData;
} else if (uploadFileHandle) {
#if DEBUG
if (uploadParams.useBackgroundSession) {
GTLR_DEBUG_LOG(@"Warning: GTLRUploadParameters should be supplied an uploadFileURL rather"
@" than a file handle to support background uploads.\n %@", uploadParams);
}
#endif
fetcher.uploadFileHandle = uploadFileHandle;
}
}
fetcher.useBackgroundSession = uploadParams.useBackgroundSession;
return fetcher;
}
#pragma mark -
- (GTLRServiceTicket *)executeBatchQuery:(GTLRBatchQuery *)batchObj
completionHandler:(GTLRServiceCompletionHandler)completionHandler
ticket:(GTLRServiceTicket *)ticket {
// Copy the original batch object and each query inside so our working queries cannot be modified
// by the caller, and release the callback blocks from the supplied query objects.
GTLRBatchQuery *batchCopy = [batchObj copy];
[batchObj invalidateQuery];
NSArray *queries = batchCopy.queries;
NSUInteger numberOfQueries = queries.count;
if (numberOfQueries == 0) return nil;
// Create the batch of REST calls.
NSMutableSet *requestIDs = [NSMutableSet setWithCapacity:numberOfQueries];
NSMutableSet *loggingNames = [NSMutableSet set];
GTMMIMEDocument *mimeDoc = [GTMMIMEDocument MIMEDocument];
// Each batch part has two "header" sections, an outer and inner.
// The inner headers are preceded by a line specifying the http request.
// So a part looks like this:
//
// --END_OF_PART
// Content-ID: gtlr_3
// Content-Transfer-Encoding: binary
// Content-Type: application/http
//
// POST https://www.googleapis.com/drive/v3/files/
// Content-Length: 0
// Content-Type: application/json
//
// {
// "id": "04109509152946699072k"
// }
for (GTLRQuery *query in queries) {
GTLRObject *bodyObject = query.bodyObject;
NSDictionary *bodyJSON = bodyObject.JSON;
NSString *requestID = query.requestID;
if (requestID.length == 0) {
GTLR_DEBUG_ASSERT(0, @"Invalid query ID: %@", [query class]);
return nil;
}
if ([requestIDs containsObject:requestID]) {
GTLR_DEBUG_ASSERT(0, @"Duplicate request ID in batch: %@", requestID);
return nil;
}
[requestIDs addObject:requestID];
// Create the inner request, body, and headers.
NSURL *requestURL = [self URLFromQueryObject:query
usePartialPaths:YES
includeServiceURLQueryParams:NO];
NSString *requestURLString = requestURL.absoluteString;
NSError *error = nil;
NSData *bodyData;
if (bodyJSON) {
bodyData = [NSJSONSerialization dataWithJSONObject:bodyJSON
options:0
error:&error];
if (bodyData == nil) {
GTLR_DEBUG_ASSERT(0, @"JSON generation error: %@\n JSON: %@", error, bodyJSON);
return nil;
}
}
NSString *httpRequestString = [NSString stringWithFormat:@"%@ %@\r\n",
query.httpMethod ?: @"GET", requestURLString];
NSDictionary *innerPartHeaders = @{ @"Content-Type" : @"application/json",
@"Content-Length" : @(bodyData.length).stringValue };
innerPartHeaders = MergeDictionaries(query.additionalHTTPHeaders, innerPartHeaders);
NSData *innerPartHeadersData = [GTMMIMEDocument dataWithHeaders:innerPartHeaders];
NSMutableData *innerData =
[[httpRequestString dataUsingEncoding:NSUTF8StringEncoding] mutableCopy];
[innerData appendData:innerPartHeadersData];
if (bodyData) {
[innerData appendData:bodyData];
}
// Combine the outer headers with the inner headers and body data.
NSDictionary *outerPartHeaders = @{ @"Content-Type" : @"application/http",
@"Content-ID" : requestID,
@"Content-Transfer-Encoding" : @"binary" };
[mimeDoc addPartWithHeaders:outerPartHeaders
body:innerData];
NSString *loggingName = query.loggingName ?: [[query class] description];
[loggingNames addObject:loggingName];
}
#if !STRIP_GTM_FETCH_LOGGING
// Set the fetcher log comment.
if (!batchCopy.loggingName) {
NSUInteger pageNumber = ticket.pagesFetchedCounter;
NSString *pageStr = @"";
if (pageNumber > 0) {
pageStr = [NSString stringWithFormat:@"page %lu, ",
(unsigned long)(pageNumber + 1)];
}
batchCopy.loggingName = [NSString stringWithFormat:@"batch: %@ (%@%lu queries)",
[loggingNames.allObjects componentsJoinedByString:@", "],
pageStr, (unsigned long)numberOfQueries];
}
#endif
dispatch_data_t mimeDispatchData;
unsigned long long mimeLength;
NSString *mimeBoundary;
[mimeDoc generateDispatchData:&mimeDispatchData
length:&mimeLength
boundary:&mimeBoundary];
batchCopy.boundary = mimeBoundary;
BOOL mayAuthorize = (batchCopy ? !batchCopy.shouldSkipAuthorization : YES);
NSString *rootURLString = self.rootURLString;
NSString *batchPath = self.batchPath ?: @"";
NSString *batchURLString = [rootURLString stringByAppendingString:batchPath];
GTLR_DEBUG_ASSERT(![batchPath hasPrefix:@"/"],
@"batchPath shouldn't start with a slash: %@",
batchPath);
// Query parameters override service parameters.
NSDictionary *mergedQueryParams = MergeDictionaries(self.additionalURLQueryParameters,
batchObj.additionalURLQueryParameters);
NSURL *batchURL;
if (mergedQueryParams.count > 0) {
batchURL = [GTLRService URLWithString:batchURLString
queryParameters:mergedQueryParams];
} else {
batchURL = [NSURL URLWithString:batchURLString];
}
GTLRServiceTicket *resultTicket = [self fetchObjectWithURL:batchURL
objectClass:[GTLRBatchResult class]
bodyObject:nil
dataToPost:(NSData *)mimeDispatchData
ETag:nil
httpMethod:@"POST"
mayAuthorize:mayAuthorize
completionHandler:completionHandler
executingQuery:batchCopy
ticket:ticket];
return resultTicket;
}
#pragma mark -
// Raw REST fetch method.
- (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)targetURL
objectClass:(Class)objectClass
bodyObject:(GTLRObject *)bodyObject
ETag:(NSString *)etag
httpMethod:(NSString *)httpMethod
mayAuthorize:(BOOL)mayAuthorize
completionHandler:(GTLRServiceCompletionHandler)completionHandler
executingQuery:(id<GTLRQueryProtocol>)executingQuery
ticket:(GTLRServiceTicket *)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 && !executingQuery.uploadParameters.shouldSendUploadOnly) {
NSError *error = nil;
NSDictionary *whatToSend;
NSDictionary *json = bodyObject.JSON;
if (json == nil) {
// Since a body object was provided, we'll ensure there's at least an empty dictionary.
json = [NSDictionary dictionary];
}
if (_dataWrapperRequired) {
// create the top-level "data" object
whatToSend = @{ @"data" : json };
} else {
whatToSend = json;
}
dataToPost = [NSJSONSerialization dataWithJSONObject:whatToSend
options:0
error:&error];
if (dataToPost == nil) {
GTLR_DEBUG_LOG(@"JSON generation error: %@", error);
}
}
return [self fetchObjectWithURL:targetURL
objectClass:objectClass
bodyObject:bodyObject
dataToPost:dataToPost
ETag:etag
httpMethod:httpMethod
mayAuthorize:mayAuthorize
completionHandler:completionHandler
executingQuery:executingQuery
ticket:ticket];
}
- (void)invokeProgressCallbackForTicket:(GTLRServiceTicket *)ticket
deliveredBytes:(unsigned long long)numReadSoFar
totalBytes:(unsigned long long)total {
GTLRServiceUploadProgressBlock block = ticket.uploadProgressBlock;
if (block) {
dispatch_group_async(ticket.callbackGroup, ticket.callbackQueue, ^{
if (ticket.cancelled) return;
block(ticket, numReadSoFar, total);
});
}
}
// 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:(GTMSessionFetcher *)fetcher
executingQuery:(id<GTLRQueryProtocol>)executingQuery
ticket:(GTLRServiceTicket *)ticket
error:(NSError *)error
defaultClass:(Class)defaultClass
completionHandler:(GTLRServiceCompletionHandler)completionHandler {
GTLR_ASSERT_CURRENT_QUEUE_DEBUG(self.parseQueue);
[ticket postNotificationOnMainThreadWithName:kGTLRServiceTicketParsingStartedNotification
object:ticket
userInfo:nil];
// For unit tests to cancel during parsing, we need a synchronous notification posted.
// Because this notification is intended only for unit tests, there is no public symbol
// for the notification name.
NSNotificationCenter *nc =[NSNotificationCenter defaultCenter];
[nc postNotificationName:@"kGTLRServiceTicketParsingStartedForTestNotification"
object:ticket
userInfo:nil];
NSDictionary *batchClassMap;
if ([executingQuery isBatchQuery]) {
// build a dictionary of expected classes for the batch responses
GTLRBatchQuery *batchQuery = (GTLRBatchQuery *)executingQuery;
NSArray *queries = batchQuery.queries;
batchClassMap = [NSMutableDictionary dictionaryWithCapacity:queries.count];
for (GTLRQuery *singleQuery in queries) {
[batchClassMap setValue:singleQuery.expectedObjectClass
forKey:singleQuery.requestID];
}
}
[self parseObjectFromDataOfFetcher:fetcher
executingQuery:executingQuery
ticket:ticket
error:error
defaultClass:defaultClass
batchClassMap:batchClassMap
hasSentParsingStartNotification:YES
completionHandler:completionHandler];
}
- (void)parseObjectFromDataOfFetcher:(GTMSessionFetcher *)fetcher
executingQuery:(id<GTLRQueryProtocol>)executingQuery
ticket:(GTLRServiceTicket *)ticket
error:(NSError *)error
defaultClass:(Class)defaultClass
batchClassMap:(NSDictionary *)batchClassMap
hasSentParsingStartNotification:(BOOL)hasSentParsingStartNotification
completionHandler:(GTLRServiceCompletionHandler)completionHandler {
GTLR_ASSERT_CURRENT_QUEUE_DEBUG(self.parseQueue);
NSError *fetchError = error;
NSString *downloadAsDataObjectType = nil;
if (![executingQuery isBatchQuery]) {
GTLRQuery *singleQuery = (GTLRQuery *)executingQuery;
downloadAsDataObjectType = singleQuery.downloadAsDataObjectType;
}
NSDictionary *responseHeaders = fetcher.responseHeaders;
NSString *contentType = [responseHeaders objectForKey:@"Content-Type"];
NSData *data = fetcher.downloadedData;
BOOL hasData = data.length > 0;
BOOL isJSON = [contentType hasPrefix:@"application/json"];
GTLRObject *parsedObject;
if (hasData) {
#if GTLR_LOG_PERFORMANCE
NSTimeInterval secs1, secs2;
secs1 = [NSDate timeIntervalSinceReferenceDate];
#endif
id<GTLRObjectClassResolver> objectClassResolver = ticket.objectClassResolver;
if ((downloadAsDataObjectType.length != 0) && fetchError == nil) {
GTLRDataObject *dataObject = [GTLRDataObject object];
dataObject.data = data;
dataObject.contentType = contentType;
parsedObject = dataObject;
} else if (isJSON) {
NSError *parseError = nil;
NSMutableDictionary *jsonWrapper =
[NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&parseError];
if (jsonWrapper == nil) {
fetchError = parseError;
} else {
NSMutableDictionary *json;
if (_dataWrapperRequired) {
json = [jsonWrapper valueForKey:@"data"];
} else {
json = jsonWrapper;
}
if (json != nil) {
parsedObject = [GTLRObject objectForJSON:json
defaultClass:defaultClass
objectClassResolver:objectClassResolver];
}
}
} else {
// Has non-JSON data; it may be batch data.
NSString *boundary;
BOOL isBatchResponse = [self isContentTypeMultipart:contentType
boundary:&boundary];
if (isBatchResponse) {
NSArray *mimeParts = [GTMMIMEDocument MIMEPartsWithBoundary:boundary
data:data];
NSArray *responseParts = [self responsePartsWithMIMEParts:mimeParts];
GTLRBatchResult *batchResult = [self batchResultWithResponseParts:responseParts
batchClassMap:batchClassMap
objectClassResolver:objectClassResolver];
parsedObject = batchResult;
} else {
GTLR_DEBUG_ASSERT(0, @"Got unexpected content type '%@'", contentType);
}
} // isJSON
#if GTLR_LOG_PERFORMANCE
secs2 = [NSDate timeIntervalSinceReferenceDate];
NSLog(@"allocation of %@ took %f seconds", objectClass, secs2 - secs1);
#endif
}
[self handleParsedObjectForFetcher:fetcher
executingQuery:executingQuery
ticket:ticket
error:fetchError
parsedObject:parsedObject
hasSentParsingStartNotification:hasSentParsingStartNotification
completionHandler:completionHandler];
}
- (void)handleParsedObjectForFetcher:(GTMSessionFetcher *)fetcher
executingQuery:(id<GTLRQueryProtocol>)executingQuery
ticket:(GTLRServiceTicket *)ticket
error:(NSError *)error
parsedObject:(GTLRObject *)object
hasSentParsingStartNotification:(BOOL)hasSentParsingStartNotification
completionHandler:(GTLRServiceCompletionHandler)completionHandler {
GTLR_ASSERT_CURRENT_QUEUE_DEBUG(self.parseQueue);
BOOL isResourceURLQuery = [executingQuery isKindOfClass:[GTLRResourceURLQuery class]];
// There may not be an object due to a fetch or parsing error
BOOL shouldFetchNextPages = ticket.shouldFetchNextPages && !isResourceURLQuery;
GTLRObject *previousObject = ticket.fetchedObject;
BOOL isFirstPage = (previousObject == nil);
if (shouldFetchNextPages && !isFirstPage && (object != nil)) {
// Accumulate new results
object = [self mergedNewResultObject:object
oldResultObject:previousObject
forQuery:executingQuery
ticket:ticket];
}
ticket.fetchedObject = object;
ticket.fetchError = error;
if (hasSentParsingStartNotification) {
// we want to always balance the start and stop notifications
[ticket postNotificationOnMainThreadWithName:kGTLRServiceTicketParsingStoppedNotification
object:ticket
userInfo:nil];
}
BOOL shouldCallCallbacks = YES;
if (error == nil) {
++ticket.pagesFetchedCounter;
// Use the nextPageToken to fetch any later pages for non-batch queries
//
// This assumes a pagination model where objects have entries in a known "items"
// field and a "nextPageToken" field, and queries support a "pageToken"
// parameter.
if (shouldFetchNextPages) {
// Determine if we should fetch more pages of results
GTLRQuery *nextPageQuery =
(GTLRQuery *)[self nextPageQueryForQuery:executingQuery
result:object
ticket:ticket];
if (nextPageQuery) {
BOOL isFetchingMore = [self fetchNextPageWithQuery:nextPageQuery
completionHandler:completionHandler
ticket:ticket];
if (isFetchingMore) {
shouldCallCallbacks = NO;
}
} else {
// nextPageQuery == nil; no more page tokens are present
#if DEBUG && !GTLR_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;
if ([executingQuery isBatchQuery]) {
queryLabel = @"batch query";
} else {
queryLabel = [[executingQuery class] description];
}
GTLR_DEBUG_LOG(@"Executing %@ query required fetching %lu pages; use a query with"
@" a larger maxResults for faster results",
queryLabel, (unsigned long)pageCount);
}
#endif
} // nextPageQuery
} else {
// !ticket.shouldFetchNextPages
#if DEBUG && !GTLR_SKIP_PAGES_WARNING
// Let the developer know that there were additional pages that would have been
// fetched if shouldFetchNextPages was enabled.
//
// The client may specify a larger page size with the query's maxResults property,
// or enable automatic pagination by turning on shouldFetchNextPages on the service
// or on the query's executionParameters.
if ([executingQuery respondsToSelector:@selector(pageToken)]
&& [object isKindOfClass:[GTLRCollectionObject class]]
&& [object respondsToSelector:@selector(nextPageToken)]
&& object.nextPageToken.length > 0) {
GTLR_DEBUG_LOG(@"Executing %@ has additional pages of results not fetched because"
@" shouldFetchNextPages is not enabled", [executingQuery class]);
}
#endif
} // ticket.shouldFetchNextPages
} // error == nil
if (!isFirstPage) {
// Release callbacks from this completed page's query.
[executingQuery invalidateQuery];
}
// 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) {
// More fetches are happening.
} else {
dispatch_group_async(ticket.callbackGroup, ticket.callbackQueue, ^{
// 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.
GTLRQuery *originalQuery = (GTLRQuery *)ticket.originalQuery;
if (!ticket.cancelled) {
if (![originalQuery isBatchQuery]) {
// Single query
GTLRServiceCompletionHandler completionBlock = originalQuery.completionBlock;
if (completionBlock) {
completionBlock(ticket, object, error);
}
} else {
[self invokeBatchCompletionsWithTicket:ticket
batchQuery:(GTLRBatchQuery *)originalQuery
batchResult:(GTLRBatchResult *)object
error:error];
}
if (completionHandler) {
completionHandler(ticket, object, error);
}
ticket.hasCalledCallback = YES;
} // !ticket.cancelled
[ticket releaseTicketCallbacks];
[ticket endBackgroundTask];
// Even if the ticket has been cancelled, it should notify that it's stopped.
[ticket notifyStarting:NO];
// Release query callback blocks.
[originalQuery invalidateQuery];
});
}
}
- (BOOL)isContentTypeMultipart:(NSString *)contentType
boundary:(NSString **)outBoundary {
NSScanner *scanner = [NSScanner scannerWithString:contentType];
// By default, the scanner skips leading whitespace.
if ([scanner scanString:@"multipart/mixed; boundary=" intoString:NULL]
&& [scanner scanUpToCharactersFromSet:[NSCharacterSet newlineCharacterSet]
intoString:outBoundary]) {
return YES;
}
return NO;
}
- (NSArray <GTLRBatchResponsePart *>*)responsePartsWithMIMEParts:(NSArray <GTMMIMEDocumentPart *>*)mimeParts {
NSMutableArray *resultParts = [NSMutableArray arrayWithCapacity:mimeParts.count];
for (GTMMIMEDocumentPart *mimePart in mimeParts) {
GTLRBatchResponsePart *responsePart = [self responsePartWithMIMEPart:mimePart];
[resultParts addObject:responsePart];
}
return resultParts;
}
- (GTLRBatchResponsePart *)responsePartWithMIMEPart:(GTMMIMEDocumentPart *)mimePart {
// The MIME part body looks like
//
// Headers (from the MIME part):
// Content-Type: application/http
// Content-ID: response-gtlr_5
//
// Body (including inner headers):
// HTTP/1.1 200 OK
// Content-Type: application/json; charset=UTF-8
// Date: Sat, 16 Jan 2016 18:57:05 GMT
// Expires: Sat, 16 Jan 2016 18:57:05 GMT
// Cache-Control: private, max-age=0
// Content-Length: 13459
//
// {"kind":"drive#fileList", ...}
GTLRBatchResponsePart *responsePart = [[GTLRBatchResponsePart alloc] init];
// The only header in the actual (outer) MIME multipart headers we want is Content-ID.
//
// The content ID in the response looks like
//
// Content-ID: response-gtlr_5
//
// but we will strip the "response-" prefix.
NSDictionary *mimeHeaders = mimePart.headers;
NSString *responseContentID = mimeHeaders[@"Content-ID"];
if ([responseContentID hasPrefix:@"response-"]) {
responseContentID = [responseContentID substringFromIndex:@"response-".length];
}
responsePart.contentID = responseContentID;
// Split the body from the inner headers at the first CRLFCRLF.
NSArray <NSNumber *>*offsets;
NSData *mimePartBody = mimePart.body;
[GTMMIMEDocument searchData:mimePartBody
targetBytes:"\r\n\r\n"
targetLength:4
foundOffsets:&offsets];
if (offsets.count == 0) {
// Parse error.
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
[userInfo setValue:mimePartBody forKey:kGTLRServiceErrorBodyDataKey];
[userInfo setValue:responseContentID forKey:kGTLRServiceErrorContentIDKey];
responsePart.parseError = [NSError errorWithDomain:kGTLRServiceErrorDomain
code:GTLRServiceErrorBatchResponseUnexpected
userInfo:userInfo];
} else {
// Separate the status/inner headers and the actual body.
NSUInteger partBodyLength = mimePartBody.length;
NSUInteger separatorOffset = offsets[0].unsignedIntegerValue;
NSData *innerHeaderData =
[mimePartBody subdataWithRange:NSMakeRange(0, (NSUInteger)separatorOffset)];
NSData *partBodyData;
if (separatorOffset + 4 < partBodyLength) {
NSUInteger offsetToBodyData = separatorOffset + 4;
NSUInteger bodyLength = mimePartBody.length - offsetToBodyData;
partBodyData = [mimePartBody subdataWithRange:NSMakeRange(offsetToBodyData, bodyLength)];
}
// Parse to separate the status line and the inner headers (though we don't
// really do much with either.)
[GTMMIMEDocument searchData:innerHeaderData
targetBytes:"\r\n"
targetLength:2
foundOffsets:&offsets];
if (offsets.count < 2) {
// Lack of status line and inner headers is strange, but not fatal since
// if the JSON was delivered.
GTLR_DEBUG_LOG(@"GTLRService: Batch result cannot parse headers for request %@:\n%@",
responseContentID,
[[NSString alloc] initWithData:innerHeaderData
encoding:NSUTF8StringEncoding]);
} else {
NSString *statusString;
NSInteger statusCode;
[self getResponseLineFromData:innerHeaderData
statusCode:&statusCode
statusString:&statusString];
responsePart.statusCode = statusCode;
responsePart.statusString = statusString;
NSUInteger actualInnerHeaderOffset = offsets[0].unsignedIntegerValue + 2;
NSData *actualInnerHeaderData;
if (innerHeaderData.length - actualInnerHeaderOffset > 0) {
NSRange actualInnerHeaderRange =
NSMakeRange(actualInnerHeaderOffset,
innerHeaderData.length - actualInnerHeaderOffset);
actualInnerHeaderData = [innerHeaderData subdataWithRange:actualInnerHeaderRange];
}
responsePart.headers = [GTMMIMEDocument headersWithData:actualInnerHeaderData];
}
// Create JSON from the body.
NSError *parseError = nil;
NSMutableDictionary *json;
if (partBodyData) {
json = [NSJSONSerialization JSONObjectWithData:partBodyData
options:NSJSONReadingMutableContainers
error:&parseError];
} else {
parseError = [NSError errorWithDomain:kGTLRServiceErrorDomain
code:GTLRServiceErrorBatchResponseUnexpected
userInfo:nil];
}
responsePart.JSON = json;
if (!json) {
// Add our content ID and part body data to the parse error.
NSMutableDictionary *userInfo =
[NSMutableDictionary dictionaryWithDictionary:parseError.userInfo];
[userInfo setValue:mimePartBody forKey:kGTLRServiceErrorBodyDataKey];
[userInfo setValue:responseContentID forKey:kGTLRServiceErrorContentIDKey];
responsePart.parseError = [NSError errorWithDomain:parseError.domain
code:parseError.code
userInfo:userInfo];
}
}
return responsePart;
}
- (void)getResponseLineFromData:(NSData *)data
statusCode:(NSInteger *)outStatusCode
statusString:(NSString **)outStatusString {
// Sample response line:
// HTTP/1.1 200 OK
*outStatusCode = -1;
*outStatusString = @"???";
NSString *responseLine = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (!responseLine) return;
NSScanner *scanner = [NSScanner scannerWithString:responseLine];
// Scanner by default skips whitespace when locating the start of the next characters to
// scan.
NSCharacterSet *wsSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
NSCharacterSet *newlineSet = [NSCharacterSet newlineCharacterSet];
NSString *httpVersion;
if ([scanner scanUpToCharactersFromSet:wsSet intoString:&httpVersion]
&& [scanner scanInteger:outStatusCode]
&& [scanner scanUpToCharactersFromSet:newlineSet intoString:outStatusString]) {
// Got it all.
}
}
- (GTLRBatchResult *)batchResultWithResponseParts:(NSArray <GTLRBatchResponsePart *>*)parts
batchClassMap:(NSDictionary *)batchClassMap
objectClassResolver:(id<GTLRObjectClassResolver>)objectClassResolver {
// Allow the resolver to override the batch rules class also.
Class resultClass =
GTLRObjectResolveClass(objectClassResolver,
[NSDictionary dictionary],
[GTLRBatchResult class]);
GTLRBatchResult *batchResult = [resultClass object];
NSMutableDictionary *successes = [NSMutableDictionary dictionary];
NSMutableDictionary *failures = [NSMutableDictionary dictionary];
NSMutableDictionary *responseHeaders = [NSMutableDictionary dictionary];
for (GTLRBatchResponsePart *responsePart in parts) {
NSString *contentID = responsePart.contentID;
NSDictionary *json = responsePart.JSON;
NSError *parseError = responsePart.parseError;
NSInteger statusCode = responsePart.statusCode;
[responseHeaders setValue:responsePart.headers forKey:contentID];
if (parseError) {
GTLRErrorObject *parseErrorObject = [GTLRErrorObject objectWithFoundationError:parseError];
[failures setValue:parseErrorObject forKey:contentID];
} else {
// There is JSON.
NSMutableDictionary *errorJSON = [json objectForKey:@"error"];
if (errorJSON) {
// A JSON error body should be the most informative error.
GTLRErrorObject *errorObject = [GTLRErrorObject objectWithJSON:errorJSON];
[failures setValue:errorObject forKey:contentID];
} else if (statusCode < 200 || statusCode > 399) {
// Report a fetch failure for this part that lacks a JSON error.
NSString *errorStr = responsePart.statusString;
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey : (errorStr ?: @"<unknown>"),
};
NSError *httpError = [NSError errorWithDomain:kGTLRServiceErrorDomain
code:GTLRServiceErrorBatchResponseStatusCode
userInfo:userInfo];
GTLRErrorObject *httpErrorObject = [GTLRErrorObject objectWithFoundationError:httpError];
[failures setValue:httpErrorObject forKey:contentID];
} else {
// The JSON represents a successful response.
Class defaultClass = batchClassMap[contentID];
id resultObject = [GTLRObject objectForJSON:[json mutableCopy]
defaultClass:defaultClass
objectClassResolver:objectClassResolver];
if (resultObject == nil) {
// Methods like delete return no object.
resultObject = [NSNull null];
}
[successes setValue:resultObject forKey:contentID];
} // errorJSON
} // parseError
} // for
batchResult.successes = successes;
batchResult.failures = failures;
batchResult.responseHeaders = responseHeaders;
return batchResult;
}
- (void)invokeBatchCompletionsWithTicket:(GTLRServiceTicket *)ticket
batchQuery:(GTLRBatchQuery *)batchQuery
batchResult:(GTLRBatchResult *)batchResult
error:(NSError *)error {
// Batch query
//
// We'll step through the queries of the original batch, not of the
// batch result
GTLR_ASSERT_CURRENT_QUEUE_DEBUG(ticket.callbackQueue);
NSDictionary *successes = batchResult.successes;
NSDictionary *failures = batchResult.failures;
for (GTLRQuery *oneQuery in batchQuery.queries) {
GTLRServiceCompletionHandler completionBlock = oneQuery.completionBlock;
if (completionBlock) {
// If there was no networking error, look for a query-specific
// error or result
GTLRObject *oneResult = nil;
NSError *oneError = error;
if (oneError == nil) {
NSString *requestID = [oneQuery requestID];
GTLRErrorObject *gtlrError = [failures objectForKey:requestID];
if (gtlrError) {
oneError = [gtlrError foundationError];
} else {
oneResult = [successes objectForKey:requestID];
if (oneResult == nil) {
// We found neither a success nor a failure for this query, unexpectedly.
GTLR_DEBUG_LOG(@"GTLRService: Batch result missing for request %@",
requestID);
oneError = [NSError errorWithDomain:kGTLRServiceErrorDomain
code:GTLRServiceErrorQueryResultMissing
userInfo:nil];
}
}
}
completionBlock(ticket, oneResult, oneError);
}
}
}
- (void)simulateFetchWithTicket:(GTLRServiceTicket *)ticket
testBlock:(GTLRServiceTestBlock)testBlock
dataToPost:(NSData *)dataToPost
completionHandler:(GTLRServiceCompletionHandler)completionHandler {
GTLRQuery *originalQuery = (GTLRQuery *)ticket.originalQuery;
ticket.executingQuery = originalQuery;
testBlock(ticket, ^(id testObject, NSError *testError) {
dispatch_group_async(ticket.callbackGroup, ticket.callbackQueue, ^{
if (!ticket.cancelled) {
if (testError) {
// During simulation, we invoke any retry block, but ignore the result.
const BOOL willRetry = NO;
GTLRServiceRetryBlock retryBlock = ticket.retryBlock;
if (retryBlock) {
(void)retryBlock(ticket, willRetry, testError);
}
} else {
// Simulate upload progress, calling back up to three times.
if (ticket.uploadProgressBlock) {
GTLRQuery *query = (GTLRQuery *)ticket.originalQuery;
unsigned long long uploadLength = [self simulatedUploadLengthForQuery:query
dataToPost:dataToPost];
unsigned long long sendReportSize = uploadLength / 3 + 1;
unsigned long long totalSentSoFar = 0;
while (totalSentSoFar < uploadLength) {
unsigned long long bytesRemaining = uploadLength - totalSentSoFar;
sendReportSize = MIN(sendReportSize, bytesRemaining);
totalSentSoFar += sendReportSize;
[self invokeProgressCallbackForTicket:ticket
deliveredBytes:(unsigned long long)totalSentSoFar
totalBytes:(unsigned long long)uploadLength];
}
[ticket postNotificationOnMainThreadWithName:kGTLRServiceTicketParsingStartedNotification
object:ticket
userInfo:nil];
[ticket postNotificationOnMainThreadWithName:kGTLRServiceTicketParsingStoppedNotification
object:ticket
userInfo:nil];
}
}
if (![originalQuery isBatchQuery]) {
// Single query
GTLRServiceCompletionHandler completionBlock = originalQuery.completionBlock;
if (completionBlock) {
completionBlock(ticket, testObject, testError);
}
} else {
// Batch query
GTLR_DEBUG_ASSERT(!testObject || [testObject isKindOfClass:[GTLRBatchResult class]],
@"Batch queries should have result objects of type GTLRBatchResult (not %@)",
[testObject class]);
[self invokeBatchCompletionsWithTicket:ticket
batchQuery:(GTLRBatchQuery *)originalQuery
batchResult:(GTLRBatchResult *)testObject
error:testError];
} // isBatchQuery
if (completionHandler) {
completionHandler(ticket, testObject, testError);
}
ticket.hasCalledCallback = YES;
} // !ticket.cancelled
// Even if the ticket has been cancelled, it should notify that it's stopped.
[ticket notifyStarting:NO];
// Release query callback blocks.
[originalQuery invalidateQuery];
}); // dispatch_group_async
}); // testBlock
}
- (unsigned long long)simulatedUploadLengthForQuery:(GTLRQuery *)query
dataToPost:(NSData *)dataToPost {
// We're uploading the body object and other posted metadata, plus optionally the
// data or file specified in the upload parameters.
unsigned long long uploadLength = dataToPost.length;
GTLRUploadParameters *uploadParameters = query.uploadParameters;
if (uploadParameters) {
NSData *uploadData = uploadParameters.data;
if (uploadData) {
uploadLength += uploadData.length;
} else {
NSURL *fileURL = uploadParameters.fileURL;
if (fileURL) {
NSError *fileError = nil;
NSNumber *fileSizeNum = nil;
if ([fileURL getResourceValue:&fileSizeNum
forKey:NSURLFileSizeKey
error:&fileError]) {
uploadLength += fileSizeNum.unsignedLongLongValue;
}
} else {
NSFileHandle *fileHandle = uploadParameters.fileHandle;
unsigned long long fileLength = [fileHandle seekToEndOfFile];
uploadLength += fileLength;
}
}
}
return uploadLength;
}
#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 <GTLRQueryProtocol>)nextPageQueryForQuery:(id<GTLRQueryProtocol>)query
result:(GTLRObject *)object
ticket:(GTLRServiceTicket *)ticket {
if (![query isBatchQuery]) {
// This is a single query
GTLRQuery *currentPageQuery = (GTLRQuery *)query;
// Determine if we should fetch more pages of results
GTLRQuery *nextPageQuery = nil;
NSString *nextPageToken = nil;
if ([object respondsToSelector:@selector(nextPageToken)]
&& [currentPageQuery respondsToSelector:@selector(pageToken)]) {
nextPageToken = [object performSelector:@selector(nextPageToken)];
}
if (nextPageToken && [object isKindOfClass:[GTLRCollectionObject class]]) {
NSString *itemsKey = [[object class] collectionItemsKey];
GTLR_DEBUG_ASSERT(itemsKey != nil, @"Missing accumulation items key for %@", [object class]);
SEL itemsSel = NSSelectorFromString(itemsKey);
if ([object respondsToSelector:itemsSel]) {
// Make a query for the next page, preserving the request ID
nextPageQuery = [currentPageQuery copy];
nextPageQuery.requestID = currentPageQuery.requestID;
[nextPageQuery performSelector:@selector(setPageToken:)
withObject:nextPageToken];
} else {
GTLR_DEBUG_ASSERT(0, @"%@ does not implement its collection items property \"%@\"",
[object class], itemsKey);
}
}
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
GTLRBatchResult *batchResult = (GTLRBatchResult *)object;
GTLRBatchQuery *nextPageBatchQuery = nil;
NSDictionary *successes = batchResult.successes;
for (NSString *requestID in successes) {
GTLRObject *singleObject = [successes objectForKey:requestID];
GTLRQuery *singleQuery = [ticket queryForRequestID:requestID];
GTLRQuery *newQuery =
(GTLRQuery *)[self nextPageQueryForQuery:singleQuery
result:singleObject
ticket:ticket];
if (newQuery) {
// There is another query to fetch
if (nextPageBatchQuery == nil) {
nextPageBatchQuery = [GTLRBatchQuery 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
//
// Returns YES if fetching of the next page has started.
- (BOOL)fetchNextPageWithQuery:(GTLRQuery *)query
completionHandler:(GTLRServiceCompletionHandler)handler
ticket:(GTLRServiceTicket *)ticket {
// Sanity check the number of pages fetched already
if (ticket.pagesFetchedCounter > kMaxNumberOfNextPagesFetched) {
// Sanity check failed: way too many pages were fetched, so the query's
// page size should be bigger to avoid driving up networking and server
// overhead.
//
// The client should be querying with a higher max results per page
// to avoid this.
GTLR_DEBUG_ASSERT(0, @"Fetched too many next pages executing %@;"
@" increase maxResults page size to avoid this.",
[query class]);
return NO;
}
GTLRServiceTicket *newTicket;
if ([query isBatchQuery]) {
newTicket = [self executeBatchQuery:(GTLRBatchQuery *)query
completionHandler:handler
ticket:ticket];
} else {
BOOL mayAuthorize = !query.shouldSkipAuthorization;
NSURL *url = [self URLFromQueryObject:query
usePartialPaths:NO
includeServiceURLQueryParams:YES];
newTicket = [self fetchObjectWithURL:url
objectClass:query.expectedObjectClass
bodyObject:query.bodyObject
ETag:nil
httpMethod:query.httpMethod
mayAuthorize:mayAuthorize
completionHandler:handler
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.
GTLR_ASSERT(newTicket == ticket || newTicket == nil,
@"Pagination should not create an additional ticket: %@", newTicket);
BOOL isFetchingNextPageWithCurrentTicket = (newTicket == ticket);
return isFetchingNextPageWithCurrentTicket;
}
// Given a new single or batch result (meaning additional pages for a previous
// query result), merge it into the old result, and return the updated object.
//
// For a single result, this inserts the old result items into the new result.
// For batch results, this replaces some of the old items with new items.
//
// This method changes the objects passed in (the old result for batches, the new result
// for individual objects.)
- (GTLRObject *)mergedNewResultObject:(GTLRObject *)newResult
oldResultObject:(GTLRObject *)oldResult
forQuery:(id<GTLRQueryProtocol>)query
ticket:(GTLRServiceTicket *)ticket {
GTLR_DEBUG_ASSERT([oldResult isMemberOfClass:[newResult class]],
@"Trying to merge %@ and %@", [oldResult class], [newResult class]);
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.
GTLRBatchResult *newBatchResult = (GTLRBatchResult *)newResult;
GTLRBatchResult *oldBatchResult = (GTLRBatchResult *)oldResult;
NSDictionary *newSuccesses = newBatchResult.successes;
if (newSuccesses.count > 0) {
NSDictionary *oldSuccesses = oldBatchResult.successes;
NSMutableDictionary *mutableOldSuccesses = [oldSuccesses mutableCopy];
for (NSString *requestID in newSuccesses) {
GTLRObject *newObj = [newSuccesses objectForKey:requestID];
GTLRObject *oldObj = [oldSuccesses objectForKey:requestID];
GTLRQuery *thisQuery = [ticket queryForRequestID:requestID];
// Recursively merge the single query's result object, appending new items to the old items.
GTLRObject *updatedObj = [self mergedNewResultObject:newObj
oldResultObject:oldObj
forQuery:thisQuery
ticket:ticket];
// In the old batch, replace the old result object with the new one.
[mutableOldSuccesses setObject:updatedObj forKey:requestID];
} // for requestID
oldBatchResult.successes = mutableOldSuccesses;
} // newSuccesses.count > 0
NSDictionary *newFailures = newBatchResult.failures;
if (newFailures.count > 0) {
NSMutableDictionary *mutableOldSuccesses = [oldBatchResult.successes mutableCopy];
NSMutableDictionary *mutableOldFailures = [oldBatchResult.failures mutableCopy];
for (NSString *requestID in newFailures) {
// In the old batch, replace old successes or failures with the new failure.
GTLRErrorObject *newError = [newFailures objectForKey:requestID];
[mutableOldFailures setObject:newError forKey:requestID];
[mutableOldSuccesses removeObjectForKey:requestID];
}
oldBatchResult.failures = mutableOldFailures;
oldBatchResult.successes = mutableOldSuccesses;
} // newFailures.count > 0
return oldBatchResult;
} else {
// Single query result
//
// Merge the items into the new object, and return the new object.
NSString *itemsKey = [[oldResult class] collectionItemsKey];
GTLR_DEBUG_ASSERT([oldResult respondsToSelector:NSSelectorFromString(itemsKey)],
@"Collection items key \"%@\" not implemented by %@", itemsKey, oldResult);
if (itemsKey) {
// Append the new items to the old items.
NSArray *oldItems = [oldResult valueForKey:itemsKey];
NSArray *newItems = [newResult valueForKey:itemsKey];
NSMutableArray *items = [NSMutableArray arrayWithArray:oldItems];
[items addObjectsFromArray:newItems];
[newResult setValue:items forKey:itemsKey];
} else {
// This shouldn't happen.
newResult = oldResult;
}
return newResult;
}
}
#pragma mark -
// GTLRQuery methods.
// Helper to create the URL from the parts.
- (NSURL *)URLFromQueryObject:(GTLRQuery *)query
usePartialPaths:(BOOL)usePartialPaths
includeServiceURLQueryParams:(BOOL)includeServiceURLQueryParams {
NSString *rootURLString = self.rootURLString;
// Skip URI template expansion if the resource URL was provided.
if ([query isKindOfClass:[GTLRResourceURLQuery class]]) {
// Because the query is created by the service rather than by the user,
// query.additionalURLQueryParameters must be nil, and usePartialPaths
// is irrelevant as the query is not in a batch.
GTLR_DEBUG_ASSERT(!usePartialPaths,
@"Batch not supported with resource URL fetch");
GTLR_DEBUG_ASSERT(!query.uploadParameters && !query.useMediaDownloadService
&& !query.downloadAsDataObjectType && !query.additionalURLQueryParameters,
@"Unsupported query properties");
NSURL *result = ((GTLRResourceURLQuery *)query).resourceURL;
if (includeServiceURLQueryParams) {
NSDictionary *additionalParams = self.additionalURLQueryParameters;
if (additionalParams.count) {
result = [GTLRService URLWithString:result.absoluteString
queryParameters:additionalParams];
}
}
return result;
}
// This is all the dance needed due to having query and path parameters for
// REST based queries.
NSDictionary *params = query.JSON;
NSString *queryFilledPathURI = [GTLRURITemplate expandTemplate:query.pathURITemplate
values:params];
// Per https://developers.google.com/discovery/v1/using#build-compose and
// https://developers.google.com/discovery/v1/using#discovery-doc-methods-mediadownload
// glue together the parts.
NSString *servicePath = self.servicePath ?: @"";
NSString *uploadPath = @"";
NSString *downloadPath = @"";
GTLR_DEBUG_ASSERT([rootURLString hasSuffix:@"/"],
@"rootURLString should end in a slash: %@", rootURLString);
GTLR_DEBUG_ASSERT(((servicePath.length == 0) ||
(![servicePath hasPrefix:@"/"] && [servicePath hasSuffix:@"/"])),
@"servicePath shouldn't start with a slash but should end with one: %@",
servicePath);
GTLR_DEBUG_ASSERT(![query.pathURITemplate hasPrefix:@"/"],
@"the queries's pathURITemplate should not start with a slash: %@",
query.pathURITemplate);
GTLRUploadParameters *uploadParameters = query.uploadParameters;
if (uploadParameters != nil) {
// If there is an override, clear all the parts and just use it with the
// the rootURLString.
NSString *override = (uploadParameters.shouldUploadWithSingleRequest
? query.simpleUploadPathURITemplateOverride
: query.resumableUploadPathURITemplateOverride);
if (override.length > 0) {
GTLR_DEBUG_ASSERT(![override hasPrefix:@"/"],
@"The query's %@UploadPathURITemplateOverride should not start with a slash: %@",
(uploadParameters.shouldUploadWithSingleRequest ? @"simple" : @"resumable"),
override);
queryFilledPathURI = [GTLRURITemplate expandTemplate:override
values:params];
servicePath = @"";
} else {
if (uploadParameters.shouldUploadWithSingleRequest) {
uploadPath = self.simpleUploadPath ?: @"";
} else {
uploadPath = self.resumableUploadPath ?: @"";
}
GTLR_DEBUG_ASSERT(((uploadPath.length == 0) ||
(![uploadPath hasPrefix:@"/"] &&
[uploadPath hasSuffix:@"/"])),
@"%@UploadPath shouldn't start with a slash but should end with one: %@",
(uploadParameters.shouldUploadWithSingleRequest ? @"simple" : @"resumable"),
uploadPath);
}
}
if (query.useMediaDownloadService &&
(query.downloadAsDataObjectType.length > 0)) {
downloadPath = @"download/";
GTLR_DEBUG_ASSERT(uploadPath.length == 0,
@"Uploading while also downloading via mediaDownService"
@" is not well defined.");
}
if (usePartialPaths) rootURLString = @"/";
NSString *urlString =
[NSString stringWithFormat:@"%@%@%@%@%@",
rootURLString, downloadPath, uploadPath, servicePath, queryFilledPathURI];
// Remove the path parameters from the dictionary.
NSMutableDictionary *workingQueryParams = [NSMutableDictionary dictionaryWithDictionary:params];
NSArray *pathParameterNames = query.pathParameterNames;
if (pathParameterNames.count > 0) {
[workingQueryParams removeObjectsForKeys:pathParameterNames];
}
// Note: A developer can override the uploadType and alt query parameters via
// query.additionalURLQueryParameters since those are added afterwards.
if (uploadParameters.shouldUploadWithSingleRequest) {
NSString *uploadType = uploadParameters.shouldSendUploadOnly ? @"media" : @"multipart";
[workingQueryParams setObject:uploadType forKey:@"uploadType"];
}
NSString *downloadAsDataObjectType = query.downloadAsDataObjectType;
if (downloadAsDataObjectType.length > 0) {
[workingQueryParams setObject:downloadAsDataObjectType
forKey:@"alt"];
}
// Add any parameters the user added directly to the query.
NSDictionary *mergedParams = MergeDictionaries(workingQueryParams,
query.additionalURLQueryParameters);
if (includeServiceURLQueryParams) {
// Query parameters override service parameters.
mergedParams = MergeDictionaries(self.additionalURLQueryParameters, mergedParams);
}
NSURL *result = [GTLRService URLWithString:urlString
queryParameters:mergedParams];
return result;
}
- (GTLRServiceTicket *)executeQuery:(id<GTLRQueryProtocol>)queryObj
delegate:(id)delegate
didFinishSelector:(SEL)finishedSelector {
GTMSessionFetcherAssertValidSelector(delegate, finishedSelector,
@encode(GTLRServiceTicket *), @encode(GTLRObject *), @encode(NSError *), 0);
GTLRServiceCompletionHandler completionHandler = ^(GTLRServiceTicket *ticket,
id object,
NSError *error) {
if (delegate && finishedSelector) {
NSMethodSignature *sig = [delegate methodSignatureForSelector:finishedSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:(SEL)finishedSelector];
[invocation setTarget:delegate];
[invocation setArgument:&ticket atIndex:2];
[invocation setArgument:&object atIndex:3];
[invocation setArgument:&error atIndex:4];
[invocation invoke];
}
};
return [self executeQuery:queryObj completionHandler:completionHandler];
}
- (GTLRServiceTicket *)executeQuery:(id<GTLRQueryProtocol>)queryObj
completionHandler:(void (^)(GTLRServiceTicket *ticket, id object,
NSError *error))handler {
if ([queryObj isBatchQuery]) {
GTLR_DEBUG_ASSERT([queryObj isKindOfClass:[GTLRBatchQuery class]],
@"GTLRBatchQuery required for batches (passed %@)",
[queryObj class]);
return [self executeBatchQuery:(GTLRBatchQuery *)queryObj
completionHandler:handler
ticket:nil];
}
GTLR_DEBUG_ASSERT([queryObj isKindOfClass:[GTLRQuery class]],
@"GTLRQuery required for single queries (passed %@)",
[queryObj class]);
// Copy the original query so our working query cannot be modified by the caller,
// and release the callback blocks from the supplied query object.
GTLRQuery *query = [(GTLRQuery *)queryObj copy];
GTLR_DEBUG_ASSERT(!query.queryInvalid, @"Query has already been executed: %@", query);
[queryObj invalidateQuery];
// For individual queries, we rely on the fetcher's log formatting so pretty-printing
// is not needed. Developers may override this in the query's additionalURLQueryParameters.
NSArray *prettyPrintNames = self.prettyPrintQueryParameterNames;
NSString *firstPrettyPrintName = prettyPrintNames.firstObject;
if (firstPrettyPrintName && (query.downloadAsDataObjectType.length == 0)
&& ![query isKindOfClass:[GTLRResourceURLQuery class]]) {
NSDictionary *queryParams = query.additionalURLQueryParameters;
BOOL foundOne = NO;
for (NSString *name in prettyPrintNames) {
if ([queryParams objectForKey:name] != nil) {
foundOne = YES;
break;
}
}
if (!foundOne) {
NSMutableDictionary *worker =
[NSMutableDictionary dictionaryWithDictionary:queryParams];
[worker setObject:@"false" forKey:firstPrettyPrintName];
query.additionalURLQueryParameters = worker;
}
}
BOOL mayAuthorize = !query.shouldSkipAuthorization;
NSURL *url = [self URLFromQueryObject:query
usePartialPaths:NO
includeServiceURLQueryParams:YES];
return [self fetchObjectWithURL:url
objectClass:query.expectedObjectClass
bodyObject:query.bodyObject
ETag:nil
httpMethod:query.httpMethod
mayAuthorize:mayAuthorize
completionHandler:handler
executingQuery:query
ticket:nil];
}
- (GTLRServiceTicket *)fetchObjectWithURL:(NSURL *)resourceURL
objectClass:(nullable Class)objectClass
executionParameters:(nullable GTLRServiceExecutionParameters *)executionParameters
completionHandler:(nullable GTLRServiceCompletionHandler)handler {
GTLRResourceURLQuery *query = [GTLRResourceURLQuery queryWithResourceURL:resourceURL
objectClass:objectClass];
query.executionParameters = executionParameters;
return [self executeQuery:query
completionHandler:handler];
}
#pragma mark -
- (NSString *)userAgent {
return _userAgent;
}
- (void)setExactUserAgent:(NSString *)userAgent {
_userAgent = [userAgent copy];
}
- (void)setUserAgent:(NSString *)userAgent {
// remove whitespace and unfriendly characters
NSString *str = GTMFetcherCleanedUserAgentString(userAgent);
[self setExactUserAgent:str];
}
- (void)overrideRequestUserAgent:(nullable NSString *)requestUserAgent {
_overrideUserAgent = [requestUserAgent copy];
}
#pragma mark -
+ (NSDictionary<NSString *, Class> *)kindStringToClassMap {
// Generated services will provide custom ones.
return [NSDictionary dictionary];
}
#pragma mark -
// The service properties becomes the initial value for each future ticket's
// properties
- (void)setServiceProperties:(NSDictionary *)dict {
_serviceProperties = [dict copy];
}
- (NSDictionary *)serviceProperties {
// be sure the returned pointer has the life of the autorelease pool,
// in case self is released immediately
__autoreleasing id props = _serviceProperties;
return props;
}
- (void)setAuthorizer:(id <GTMFetcherAuthorizationProtocol>)authorizer {
self.fetcherService.authorizer = authorizer;
}
- (id <GTMFetcherAuthorizationProtocol>)authorizer {
return self.fetcherService.authorizer;
}
+ (NSUInteger)defaultServiceUploadChunkSize {
// Subclasses may override this method.
// The upload server prefers multiples of 256K.
const NSUInteger kMegabyte = 4 * 256 * 1024;
#if TARGET_OS_IPHONE
// For iOS, we're balancing a large upload size with limiting the memory
// used for the upload data buffer.
return 4 * kMegabyte;
#else
// A large upload chunk size minimizes http overhead and server effort.
return 25 * kMegabyte;
#endif
}
- (NSUInteger)serviceUploadChunkSize {
if (_uploadChunkSize > 0) {
return _uploadChunkSize;
}
return [[self class] defaultServiceUploadChunkSize];
}
- (void)setServiceUploadChunkSize:(NSUInteger)val {
_uploadChunkSize = val;
}
- (void)setSurrogates:(NSDictionary <Class, Class>*)surrogates {
NSDictionary *kindMap = [[self class] kindStringToClassMap];
self.objectClassResolver = [GTLRObjectClassResolver resolverWithKindMap:kindMap
surrogates:surrogates];
}
#pragma mark - Internal helper
// If there are already query parameters on urlString, the new ones are simply
// appended after them.
+ (NSURL *)URLWithString:(NSString *)urlString
queryParameters:(NSDictionary *)queryParameters {
if (urlString.length == 0) return nil;
NSString *fullURLString;
if (queryParameters.count > 0) {
// Use GTLRURITemplate by building up a template and then feeding in the
// values. The template is query expansion ('?'), and any key that is
// an array or dictionary gets tagged to explode them ('+').
NSArray *sortedQueryParamKeys =
[queryParameters.allKeys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
NSMutableString *template = [@"{" mutableCopy];
char joiner = '?';
for (NSString *key in sortedQueryParamKeys) {
[template appendFormat:@"%c%@", joiner, key];
id value = [queryParameters objectForKey:key];
if ([value isKindOfClass:[NSArray class]] ||
[value isKindOfClass:[NSDictionary class]]) {
[template appendString:@"+"];
}
joiner = ',';
}
[template appendString:@"}"];
NSString *urlArgs =
[GTLRURITemplate expandTemplate:template
values:queryParameters];
urlArgs = [urlArgs substringFromIndex:1]; // Drop the '?' and use the joiner.
BOOL missingQMark = ([urlString rangeOfString:@"?"].location == NSNotFound);
joiner = missingQMark ? '?' : '&';
fullURLString =
[NSString stringWithFormat:@"%@%c%@", urlString, joiner, urlArgs];
} else {
fullURLString = urlString;
}
NSURL *result = [NSURL URLWithString:fullURLString];
return result;
}
@end
@implementation GTLRService (TestingSupport)
+ (instancetype)mockServiceWithFakedObject:(id)objectOrNil
fakedError:(NSError *)errorOrNil {
GTLRService *service = [[GTLRService alloc] init];
service.rootURLString = @"https://example.invalid/";
service.testBlock = ^(GTLRServiceTicket *ticket, GTLRServiceTestResponse testResponse) {
testResponse(objectOrNil, errorOrNil);
};
return service;
}
- (BOOL)waitForTicket:(GTLRServiceTicket *)ticket
timeout:(NSTimeInterval)timeoutInSeconds {
// Loop until the fetch completes or is cancelled, or until the timeout has expired.
NSDate *giveUpDate = [NSDate dateWithTimeIntervalSinceNow:timeoutInSeconds];
BOOL hasTimedOut = NO;
while (1) {
int64_t delta = (int64_t)(100 * NSEC_PER_MSEC); // 100 ms
BOOL areCallbacksPending =
(dispatch_group_wait(ticket.callbackGroup, dispatch_time(DISPATCH_TIME_NOW, delta)) != 0);
if (!areCallbacksPending && (ticket.hasCalledCallback || ticket.cancelled)) break;
hasTimedOut = (giveUpDate.timeIntervalSinceNow <= 0);
if (hasTimedOut) {
if (areCallbacksPending) {
// A timeout while waiting for the dispatch group to finish is seriously unexpected.
GTLR_DEBUG_LOG(@"%s timed out while waiting for the dispatch group", __PRETTY_FUNCTION__);
} else {
GTLR_DEBUG_LOG(@"%s timed out without callbacks pending", __PRETTY_FUNCTION__);
}
break;
}
// 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];
}
return !hasTimedOut;
}
@end
@implementation GTLRServiceTicket {
GTLRService *_service;
NSDictionary *_ticketProperties;
GTLRServiceUploadProgressBlock _uploadProgressBlock;
BOOL _needsStopNotification;
}
@synthesize APIKey = _apiKey,
APIKeyRestrictionBundleID = _apiKeyRestrictionBundleID,
allowInsecureQueries = _allowInsecureQueries,
authorizer = _authorizer,
cancelled = _cancelled,
callbackGroup = _callbackGroup,
callbackQueue = _callbackQueue,
creationDate = _creationDate,
executingQuery = _executingQuery,
fetchedObject = _fetchedObject,
fetchError = _fetchError,
fetchRequest = _fetchRequest,
fetcherService = _fetcherService,
hasCalledCallback = _hasCalledCallback,
maxRetryInterval = _maxRetryInterval,
objectFetcher = _objectFetcher,
originalQuery = _originalQuery,
pagesFetchedCounter = _pagesFetchedCounter,
postedObject = _postedObject,
retryBlock = _retryBlock,
retryEnabled = _retryEnabled,
shouldFetchNextPages = _shouldFetchNextPages,
objectClassResolver = _objectClassResolver,
testBlock = _testBlock;
#if GTM_BACKGROUND_TASK_FETCHING
@synthesize backgroundTaskIdentifier = _backgroundTaskIdentifier;
#endif
#if DEBUG
- (instancetype)init {
[self doesNotRecognizeSelector:_cmd];
self = nil;
return self;
}
#endif
#if GTM_BACKGROUND_TASK_FETCHING && DEBUG
- (void)dealloc {
GTLR_DEBUG_ASSERT(_backgroundTaskIdentifier == UIBackgroundTaskInvalid,
@"Background task not ended");
}
#endif // GTM_BACKGROUND_TASK_FETCHING && DEBUG
- (instancetype)initWithService:(GTLRService *)service
executionParameters:(GTLRServiceExecutionParameters *)params {
self = [super init];
if (self) {
// ivars set at init time and never changed are exposed as atomic readonly properties.
_service = service;
_fetcherService = service.fetcherService;
_authorizer = service.authorizer;
_ticketProperties = MergeDictionaries(service.serviceProperties, params.ticketProperties);
_objectClassResolver = params.objectClassResolver ?: service.objectClassResolver;
_retryEnabled = ((params.retryEnabled != nil) ? params.retryEnabled.boolValue : service.retryEnabled);
_maxRetryInterval = ((params.maxRetryInterval != nil) ?
params.maxRetryInterval.doubleValue : service.maxRetryInterval);
_shouldFetchNextPages = ((params.shouldFetchNextPages != nil)?
params.shouldFetchNextPages.boolValue : service.shouldFetchNextPages);
GTLRServiceUploadProgressBlock uploadProgressBlock =
params.uploadProgressBlock ?: service.uploadProgressBlock;
_uploadProgressBlock = [uploadProgressBlock copy];
GTLRServiceRetryBlock retryBlock = params.retryBlock ?: service.retryBlock;
_retryBlock = [retryBlock copy];
if (_retryBlock) {
_retryEnabled = YES;
}
_testBlock = params.testBlock ?: service.testBlock;
_callbackQueue = ((_Nonnull dispatch_queue_t)params.callbackQueue) ?: service.callbackQueue;
_callbackGroup = dispatch_group_create();
_apiKey = [service.APIKey copy];
_apiKeyRestrictionBundleID = [service.APIKeyRestrictionBundleID copy];
_allowInsecureQueries = service.allowInsecureQueries;
#if GTM_BACKGROUND_TASK_FETCHING
_backgroundTaskIdentifier = UIBackgroundTaskInvalid;
#endif
_creationDate = [NSDate date];
}
return self;
}
- (NSString *)description {
NSString *devKeyInfo = @"";
if (_apiKey != nil) {
devKeyInfo = [NSString stringWithFormat:@" devKey:%@", _apiKey];
}
NSString *keyRestrictionInfo = @"";
if (_apiKeyRestrictionBundleID != nil) {
keyRestrictionInfo = [NSString stringWithFormat:@" restriction:%@",
_apiKeyRestrictionBundleID];
}
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, keyRestrictionInfo, authorizerInfo, _objectFetcher];
}
- (void)postNotificationOnMainThreadWithName:(NSString *)name
object:(id)object
userInfo:(NSDictionary *)userInfo {
// We always post these async to ensure they remain in order.
dispatch_group_async(self.callbackGroup, dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:name
object:object
userInfo:userInfo];
});
}
- (void)pauseUpload {
GTMSessionFetcher *fetcher = self.objectFetcher;
BOOL canPause = [fetcher respondsToSelector:@selector(pauseFetching)];
GTLR_DEBUG_ASSERT(canPause, @"tickets can be paused only for chunked resumable uploads");
if (canPause) {
[(GTMSessionUploadFetcher *)fetcher pauseFetching];
}
}
- (void)resumeUpload {
GTMSessionFetcher *fetcher = self.objectFetcher;
BOOL canResume = [fetcher respondsToSelector:@selector(resumeFetching)];
GTLR_DEBUG_ASSERT(canResume, @"tickets can be resumed only for chunked resumable uploads");
if (canResume) {
[(GTMSessionUploadFetcher *)fetcher resumeFetching];
}
}
- (BOOL)isUploadPaused {
BOOL isPausable = [_objectFetcher respondsToSelector:@selector(isPaused)];
GTLR_DEBUG_ASSERT(isPausable, @"tickets can be paused only for chunked resumable uploads");
if (isPausable) {
return [(GTMSessionUploadFetcher *)_objectFetcher isPaused];
}
return NO;
}
- (BOOL)isCancelled {
@synchronized(self) {
return _cancelled;
}
}
- (void)cancelTicket {
@synchronized(self) {
_cancelled = YES;
}
[_objectFetcher stopFetching];
self.objectFetcher = nil;
self.fetchRequest = nil;
_ticketProperties = nil;
[self releaseTicketCallbacks];
[self endBackgroundTask];
[self.executingQuery invalidateQuery];
id<GTLRQueryProtocol> originalQuery = self.originalQuery;
self.executingQuery = originalQuery;
[originalQuery invalidateQuery];
_service = nil;
_fetcherService = nil;
_authorizer = nil;
_testBlock = nil;
}
#if GTM_BACKGROUND_TASK_FETCHING
// When the fetcher's substitute UIApplication object is present, GTLRService
// will use that instead of UIApplication. This is just to reduce duplicating
// that plumbing for testing.
+ (nullable id<GTMUIApplicationProtocol>)fetcherUIApplication {
id<GTMUIApplicationProtocol> app = [GTMSessionFetcher substituteUIApplication];
if (app) return app;
static Class applicationClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
BOOL isAppExtension = [[[NSBundle mainBundle] bundlePath] hasSuffix:@".appex"];
if (!isAppExtension) {
Class cls = NSClassFromString(@"UIApplication");
if (cls && [cls respondsToSelector:NSSelectorFromString(@"sharedApplication")]) {
applicationClass = cls;
}
}
});
if (applicationClass) {
app = (id<GTMUIApplicationProtocol>)[applicationClass sharedApplication];
}
return app;
}
#endif // GTM_BACKGROUND_TASK_FETCHING
- (void)startBackgroundTask {
#if GTM_BACKGROUND_TASK_FETCHING
GTLR_DEBUG_ASSERT(self.backgroundTaskIdentifier == UIBackgroundTaskInvalid,
@"Redundant GTLRService background task: %lu",
(unsigned long)self.backgroundTaskIdentifier);
NSString *taskName = [[self.executingQuery class] description];
id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
// We'll use a locally-scoped task ID variable so the expiration block is guaranteed
// to refer to this task rather than to whatever task the property has.
__block UIBackgroundTaskIdentifier bgTaskID =
[app beginBackgroundTaskWithName:taskName
expirationHandler:^{
// Background task expiration callback. This block is always invoked by
// UIApplication on the main thread.
if (bgTaskID != UIBackgroundTaskInvalid) {
@synchronized(self) {
if (bgTaskID == self.backgroundTaskIdentifier) {
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
}
// This explicitly ends the captured bgTaskID rather than the backgroundTaskIdentifier
// property to ensure expiration is handled even if the property has changed.
[app endBackgroundTask:bgTaskID];
}
}];
@synchronized(self) {
self.backgroundTaskIdentifier = bgTaskID;
}
#endif // GTM_BACKGROUND_TASK_FETCHING
}
- (void)endBackgroundTask {
#if GTM_BACKGROUND_TASK_FETCHING
// Whenever the connection stops or a next page is about to be fetched,
// tell UIApplication we're done.
UIBackgroundTaskIdentifier bgTaskID;
@synchronized(self) {
bgTaskID = self.backgroundTaskIdentifier;
self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
}
if (bgTaskID != UIBackgroundTaskInvalid) {
[[[self class] fetcherUIApplication] endBackgroundTask:bgTaskID];
}
#endif // GTM_BACKGROUND_TASK_FETCHING
}
- (void)releaseTicketCallbacks {
self.uploadProgressBlock = nil;
self.retryBlock = nil;
}
- (void)notifyStarting:(BOOL)isStarting {
GTLR_DEBUG_ASSERT(!GTLR_AreBoolsEqual(isStarting, _needsStopNotification),
@"Notification mismatch (isStarting=%d)", isStarting);
if (GTLR_AreBoolsEqual(isStarting, _needsStopNotification)) return;
NSString *name;
if (isStarting) {
name = kGTLRServiceTicketStartedNotification;
_needsStopNotification = YES;
} else {
name = kGTLRServiceTicketStoppedNotification;
_needsStopNotification = NO;
}
[self postNotificationOnMainThreadWithName:name
object:self
userInfo:nil];
}
- (id)service {
return _service;
}
- (void)setObjectFetcher:(GTMSessionFetcher *)fetcher {
@synchronized(self) {
_objectFetcher = fetcher;
}
[self updateObjectFetcherProgressCallbacks];
}
- (GTMSessionFetcher *)objectFetcher {
@synchronized(self) {
return _objectFetcher;
}
}
- (NSDictionary *)ticketProperties {
// be sure the returned pointer has the life of the autorelease pool,
// in case self is released immediately
__autoreleasing id props = _ticketProperties;
return props;
}
- (GTLRServiceUploadProgressBlock)uploadProgressBlock {
return _uploadProgressBlock;
}
- (void)setUploadProgressBlock:(GTLRServiceUploadProgressBlock)block {
if (_uploadProgressBlock != block) {
_uploadProgressBlock = [block copy];
[self updateObjectFetcherProgressCallbacks];
}
}
- (void)updateObjectFetcherProgressCallbacks {
// Internal method. Do not override.
GTMSessionFetcher *fetcher = [self objectFetcher];
if (_uploadProgressBlock) {
// Use a local block variable to avoid a spurious retain cycle warning.
GTMSessionFetcherSendProgressBlock fetcherSentDataBlock = ^(int64_t bytesSent,
int64_t totalBytesSent,
int64_t totalBytesExpectedToSend) {
[self->_service invokeProgressCallbackForTicket:self
deliveredBytes:(unsigned long long)totalBytesSent
totalBytes:(unsigned long long)totalBytesExpectedToSend];
};
fetcher.sendProgressBlock = fetcherSentDataBlock;
} else {
fetcher.sendProgressBlock = nil;
}
}
- (NSInteger)statusCode {
return [_objectFetcher statusCode];
}
- (GTLRQuery *)queryForRequestID:(NSString *)requestID {
id<GTLRQueryProtocol> queryObj = self.executingQuery;
if ([queryObj isBatchQuery]) {
GTLRBatchQuery *batch = (GTLRBatchQuery *)queryObj;
GTLRQuery *result = [batch queryForRequestID:requestID];
return result;
} else {
GTLR_DEBUG_ASSERT(0, @"just use ticket.executingQuery");
return nil;
}
}
@end
@implementation GTLRServiceExecutionParameters
@synthesize maxRetryInterval = _maxRetryInterval,
retryEnabled = _retryEnabled,
retryBlock = _retryBlock,
shouldFetchNextPages = _shouldFetchNextPages,
objectClassResolver = _objectClassResolver,
testBlock = _testBlock,
ticketProperties = _ticketProperties,
uploadProgressBlock = _uploadProgressBlock,
callbackQueue = _callbackQueue;
- (id)copyWithZone:(NSZone *)zone {
GTLRServiceExecutionParameters *newObject = [[self class] allocWithZone:zone];
newObject.maxRetryInterval = self.maxRetryInterval;
newObject.retryEnabled = self.retryEnabled;
newObject.retryBlock = self.retryBlock;
newObject.shouldFetchNextPages = self.shouldFetchNextPages;
newObject.objectClassResolver = self.objectClassResolver;
newObject.testBlock = self.testBlock;
newObject.ticketProperties = self.ticketProperties;
newObject.uploadProgressBlock = self.uploadProgressBlock;
newObject.callbackQueue = self.callbackQueue;
return newObject;
}
- (BOOL)hasParameters {
if (self.maxRetryInterval != nil) return YES;
if (self.retryEnabled != nil) return YES;
if (self.retryBlock) return YES;
if (self.shouldFetchNextPages != nil) return YES;
if (self.objectClassResolver) return YES;
if (self.testBlock) return YES;
if (self.ticketProperties) return YES;
if (self.uploadProgressBlock) return YES;
if (self.callbackQueue) return YES;
return NO;
}
@end
@implementation GTLRResourceURLQuery
@synthesize resourceURL = _resourceURL;
+ (instancetype)queryWithResourceURL:(NSURL *)resourceURL
objectClass:(Class)objectClass {
GTLRResourceURLQuery *query = [[self alloc] initWithPathURITemplate:@"_usingGTLRResourceURLQuery_"
HTTPMethod:nil
pathParameterNames:nil];
query.expectedObjectClass = objectClass;
query.resourceURL = resourceURL;
return query;
}
- (instancetype)copyWithZone:(NSZone *)zone {
GTLRResourceURLQuery *result = [super copyWithZone:zone];
result->_resourceURL = self->_resourceURL;
return result;
}
// TODO: description
@end
@implementation GTLRObjectCollectionImpl
@dynamic nextPageToken;
@end