1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884 |
- /* 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.
- */
- #import <TargetConditionals.h>
- #if TARGET_OS_IPHONE
- #import <UIKit/UIKit.h>
- #endif
- //#import <GoogleAPIClientForREST/GTLRService.h>
- #import "GTLRService.h"
- //#import <GoogleAPIClientForREST/GTLRFramework.h>
- //#import <GoogleAPIClientForREST/GTLRURITemplate.h>
- //#import <GoogleAPIClientForREST/GTLRUtilities.h>
- #import "GTLRFramework.h"
- #import "GTLRURITemplate.h"
- #import "GTLRUtilities.h"
- #import "GTLRDefines.h"
- // TODO: Simplify when the 2.0 SessionFetcher is the min dependency.
- #if __has_include(<GTMSessionFetcher/GTMSessionUploadFetcher.h>) // 2.x & CocoaPods
- #import <GTMSessionFetcher/GTMSessionUploadFetcher.h>
- #import <GTMSessionFetcher/GTMMIMEDocument.h>
- #else // SwiftPM 1.x
- // #import "../GTMSessionUploadFetcher.h"
- // #import "../GTMMIMEDocument.h"
- #import "GTMSessionUploadFetcher.h"
- #import "GTMMIMEDocument.h"
- #endif
- #ifndef STRIP_GTM_FETCH_LOGGING
- #error GTMSessionFetcher headers should have defaulted this if it wasn't already defined.
- #endif
- #ifndef GTLR_ASSERT_CURRENT_QUEUE_DEBUG
- #if DEBUG && !defined(NS_BLOCK_ASSERTIONS)
- static __inline__ __attribute__((always_inline))
- void GTLR_ASSERT_CURRENT_QUEUE_DEBUG_IMPL(dispatch_queue_t targetQueue) {
- if (@available(iOS 10, *)) {
- dispatch_assert_queue(targetQueue);
- }
- }
- #define GTLR_ASSERT_CURRENT_QUEUE_DEBUG(targetQueue) \
- GTLR_ASSERT_CURRENT_QUEUE_DEBUG_IMPL(targetQueue)
- #else
- #define GTLR_ASSERT_CURRENT_QUEUE_DEBUG(targetQueue) do { } while (0)
- #endif // DEBUG && !defined(NS_BLOCK_ASSERTIONS)
- #endif // GTLR_ASSERT_CURRENT_QUEUE_DEBUG
- 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;
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated"
- @property(nonatomic, strong, nullable) id<GTMFetcherAuthorizationProtocol> authorizer;
- #pragma clang diagnostic pop
- // 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
- @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. The check for the specific bundle is basically a noop as
- // it was the hardcoded value from the framework when the project included
- // and Xcode project. It is kept just incase someone happened to use the
- // same bundle id so the behavior remains consistent.
- 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 *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 = [GTMSessionUploadFetcher uploadFetcherWithLocation:uploadLocationURL
- uploadMIMEType:uploadMIMEType
- chunkSize:(int64_t)uploadChunkSize
- fetcherService:fetcherService];
- fetcher.uploadFileURL = uploadFileURL;
- } else {
- fetcher = [GTMSessionUploadFetcher 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];
- NSData *statusLine;
- NSData *actualInnerHeaderData;
- if (offsets.count) {
- NSRange statusRange = NSMakeRange(0, offsets[0].unsignedIntegerValue);
- statusLine = [innerHeaderData subdataWithRange:statusRange];
- NSUInteger actualInnerHeaderOffset = offsets[0].unsignedIntegerValue + 2;
- if (innerHeaderData.length - actualInnerHeaderOffset > 0) {
- NSRange actualInnerHeaderRange =
- NSMakeRange(actualInnerHeaderOffset,
- innerHeaderData.length - actualInnerHeaderOffset);
- actualInnerHeaderData = [innerHeaderData subdataWithRange:actualInnerHeaderRange];
- }
- } else {
- // There appears to only be a status line.
- //
- // This means there were no reponse headers. "Date" seems like it should
- // be required, but https://tools.ietf.org/html/rfc7231#section-7.1.1.2
- // lets even that be left off if a server doesn't have a clock it knows
- // to be correct.
- statusLine = innerHeaderData;
- }
- NSString *statusString;
- NSInteger statusCode;
- [self getResponseLineFromData:statusLine
- statusCode:&statusCode
- statusString:&statusString];
- responsePart.statusCode = statusCode;
- responsePart.statusString = statusString;
- if (actualInnerHeaderData) {
- responsePart.headers = [GTMMIMEDocument headersWithData:actualInnerHeaderData];
- }
- // Create JSON from the body.
- // (if there is any, methods like delete return nothing)
- NSMutableDictionary *json;
- if (partBodyData) {
- NSError *parseError = nil;
- json = [NSJSONSerialization JSONObjectWithData:partBodyData
- options:NSJSONReadingMutableContainers
- error:&parseError];
- if (!json) {
- if (!parseError) {
- // There should be an error, but just incase...
- parseError = [NSError errorWithDomain:kGTLRServiceErrorDomain
- code:GTLRServiceErrorBatchResponseUnexpected
- userInfo:nil];
- }
- // 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];
- }
- }
- responsePart.JSON = json;
- }
- 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.
- #if DEBUG
- if (![httpVersion hasPrefix:@"HTTP/"]) {
- GTLR_DEBUG_LOG(@"GTLRService: Non-standard HTTP Version: %@", httpVersion);
- }
- #endif
- }
- }
- - (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.
- NSAssert(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" : @"Redefine"),
- 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;
- }
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated"
- - (void)setAuthorizer:(id <GTMFetcherAuthorizationProtocol>)authorizer {
- self.fetcherService.authorizer = authorizer;
- }
- - (id <GTMFetcherAuthorizationProtocol>)authorizer {
- return self.fetcherService.authorizer;
- }
- #pragma clang diagnostic pop
- + (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 = @"";
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated"
- id <GTMFetcherAuthorizationProtocol> authorizer = self.objectFetcher.authorizer;
- #pragma clang diagnostic pop
- 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.
- // Since a request can be started from any thread, we also have to ensure the
- // variable for accessing it is safe across the initial thread and the handler
- // (incase it gets failed immediately from the app already heading into the
- // background).
- __block UIBackgroundTaskIdentifier guardedTaskID = UIBackgroundTaskInvalid;
- UIBackgroundTaskIdentifier returnedTaskID =
- [app beginBackgroundTaskWithName:taskName
- expirationHandler:^{
- // Background task expiration callback. This block is always invoked by
- // UIApplication on the main thread.
- UIBackgroundTaskIdentifier localTaskID;
- @synchronized(self) {
- localTaskID = guardedTaskID;
- }
- if (localTaskID != UIBackgroundTaskInvalid) {
- @synchronized(self) {
- if (localTaskID == self.backgroundTaskIdentifier) {
- self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- }
- // This explicitly ends the captured localTaskID rather than the backgroundTaskIdentifier
- // property to ensure expiration is handled even if the property has changed.
- [app endBackgroundTask:localTaskID];
- }
- }];
- @synchronized(self) {
- guardedTaskID = returnedTaskID;
- self.backgroundTaskIdentifier = returnedTaskID;
- }
- #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
|