GBA003/Pods/GoogleAPIClientForREST/Source/Objects/GTLRService.m
2024-05-30 10:22:15 +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