2887 lines
114 KiB
Objective-C
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
|