|
|
Introduction to Google Data APIs for Cocoa Developers
Google Data APIs allow client software to access and manipulate data hosted by Google services.
The Google Data APIs Objective-C Client Library is a Mac OS X framework that enables developers for Mac OS X to easily write native Cocoa applications. The framework handles
- XML parsing and generation
- Networking
- Sign-in for Google accounts
- Service-specific protocols and query generation
Requirements
The Google Data APIs Objective-C Client Library requires Mac OS X 10.4 due to dependencies on NSXMLDocument.
Example Applications
The Examples directory contains example applications showing typical interactions with Google services using the framework. The applications act as simple browsers for the structure of feed and entry classes for each service. The WindowController source files of the samples were written with typical Cocoa idioms to serve as quick introductions to use of the APIs.
Adding Google Data APIs to a project
The Google Data APIs Objective-C Client Library is provided as a built framework, suitable for inclusion in a Cocoa application bundle's Frameworks folder. To add the framework to an Xcode project, drag GData.framework to the project's Linked Frameworks source group, then drag the GData framework from the Linked Frameworks group folder to the Link Binary With Library phase inside of the application target.
Source files referring to GData objects should include either the full GData headers as
#import "GData/GData.h"
or the header for a specific service, such as
#import "GData/GDataCalendar.h"
The GData source code and the project for building the framework are also provided. To facilitate debugging, you may opt to include the GData.xcode project or the GData source files directly in your application project. The example applications show how to include a reference to the GData framework project file in your Xcode project.
Google Data APIs Basics
Servers respond to client GData requests with feeds that include lists of entries. For example, a request for all of a user's calendars would return a Calendar feed with a list of entries, where each entry represents one calendar. A request for all events in one calendar would return a Calendar Event feed with a list of entries, with each entry representing one of the user's scheduled events.
Each feed and entry is composed of elements. Elements represent either standard Atom XML elements, or custom-defined GData elements.
Feeds, entries, and elements are derived from GDataObject, the base class that implements XML parsing and generation.
Google web application interactions are handled by service objects. A single transaction with a service is tracked with a service ticket.
For example, here is how to use the Google Calendar service to retrieve a feed of calendar entries, where each entry describes one of the user's calendars.
service = [[GDataServiceGoogleCalendar alloc] init];
[service setUserAgent:@"MyCompanyName-MyCalendarAppName-1.0"];
[service setUserCredentialsWithUsername:username
password:password];
GDataServiceTicket *ticket;
ticket = [service fetchCalendarFeedForUsername:username
delegate:self
didFinishSelector:@selector(ticket:finishedWithFeed:)
didFailSelector:@selector(ticket:failedWithError:)];Service objects maintain cookies and track data modification dates to minimize server loads, so it's best to reuse a service object for sequences of server requests.
The application may choose to retain the ticket following the fetch call so the user can cancel the service request. The ticket is valid so long as the application retains it. To cancel a fetch in progress, call [ticket cancelTicket]. Once the finished or failed callback has been called, the ticket is no longer useful and may be released.
Here is what the callback from a successful fetch of the calendar list might look like. This callback example just prints the title of the user's first calendar.
- (void)ticket:(GDataServiceTicket *)ticket
finishedWithFeed:(GDataFeedCalendar *)feed {
if ([[feed entries] count] > 0) {
GDataEntryCalendar *firstCalendar = [[feed entries] objectAtIndex:0];
GDataTextConstruct *titleTextConstruct = [firstCalendar title];
NSString *title = [titleTextConstruct stringValue];
NSLog(@"first calendar's title:%@", title);
}
}Service objects include a variety of methods for interacting with the service. Typically, the interactions include some or all of these activities:
- fetching a feed
- inserting an entry into a feed
- updating (replacing) an entry in a feed
- deleting an entry or a feed
- performing queries to obtain a subset of the feed's entries
- performing a batch operation of inserting, updating, and deleting entries
Feeds and entries typically contain links to themselves or to other objects. The library provides convenience categories on NSArray for retrieving individual links. For example, to retrieve the events for a user's calendar, use a Calendar service object to fetch from the Calendar's "alternate" link:
NSArray *entryLinks = [firstCalendar links];
GDataLink *link = [entryLinks alternateLink];
NSURL* linkURL = [link URL];
if (linkURL != nil) {
[service fetchCalendarEventFeedWithURL:linkURL
delegate:self
didFinishSelector:@selector(eventsTicket:finishedWithEntries:)
didFailSelector:@selector(eventsTicket:failedWithError:)];
}Typically, the alternate link points to an html or user-friendly representation of the data, though Google Calendar uses it as a link to the event feed for a calendar.
Modifiable feeds may have a post link, which contains the URL for inserting new entries into the feed. Modifiable entries have an edit link, which is used to update or delete the entry. Entries that refer to non-XML data, such as photographs, may include an edit media link for modifying or deleting the media data. Both entries and feeds have self links, which self-referentially contain the URL of the XML for the entry or feed.
A particularly important link in feeds is the next link; it is present when the feed contains only a partial set of results from the request. If the feed's [links nextLink] is present, the client application must perform a new request using the "next" link URL to retrieve additional entries.
To make the library's service object follow "next" links automatically, call [service setServiceShouldFollowNextLinks:YES]. The returned ticket then applies to the sequence of http requests needed to obtain the feed entries.
Note, however, that large feeds may take a long time to be retrieved, as each "next" link will lead to a new http request. Assigning a large value to a feed query's maxResults argument will give results faster than will following "next" links. Additionally, accumulating entries into a feed by following "next" links means that the feed's elements reflect only one of the fetches, even though the entries array contains the entries returned by multiple requests.
Service-specific query objects can generate URLs with parameters appropriate for restricting a feed's entries. For example, a query could request a feed of Calendar events between specific dates, or of database items of a specified category. Here is an example of a query to retrieve the first 5 events from a user's calendar:
- (void)beginFetchingFiveEventsFromCalendar:(GDataEntryCalendar *)calendar {
NSURL *feedURL = [[[calendar links] alternateLink] URL];
GDataQueryCalendar* queryCal = [GDataQueryCalendar calendarQueryWithFeedURL:feedURL];
[queryCal setStartIndex:1];
[queryCal setMaxResults:5];
GDataServiceGoogleCalendar *service = [self calendarService];
[service fetchCalendarQuery:queryCal
delegate:self
didFinishSelector:@selector(queryTicket:finishedWithEntries:)
didFailSelector:@selector(queryTicket:failedWithError:)];
}Creating GDataObjects from scratch
Typically GDataObjects are created by the framework from XML returned from a server, but occasionally it is useful to create one from scratch. This snippet shows how to create a new event to add to a user's calendar:
- (void)addAnEventToCalendar:(GDataEntryCalendar *)calendar {
// make a new event
GDataEntryCalendarEvent *newEvent = [GDataEntryCalendarEvent calendarEvent];
// set a title, description, and author
[newEvent setTitleWithString:@"Meeting"];
[newEvent setSummaryWithString:@"Today's discussion"];
GDataPerson *authorPerson = [GDataPerson personWithName:@"Fred Flintstone"
email:@"fred.flinstone@spurious.xxx.com"];
[newEvent addAuthor:authorPerson];
// start time now, end time in an hour
NSDate *anHourFromNow = [NSDate dateWithTimeIntervalSinceNow:60*60];
GDataDateTime *startDateTime = [GDataDateTime dateTimeWithDate:[NSDate date]
timeZone:[NSTimeZone systemTimeZone]];
GDataDateTime *endDateTime = [GDataDateTime dateTimeWithDate:anHourFromNow
timeZone:[NSTimeZone systemTimeZone]];
// reminder 10 minutes before the event
GDataReminder *reminder = [GDataReminder reminder];
[reminder setMinutes:@"10"];
GDataWhen *when = [GDataWhen whenWithStartTime:startDateTime
endTime:endDateTime];
[when addReminders:reminder];
[newEvent addTime:when];
// add it to the user's calendar
NSURL *feedURL = [[[calendar links] alternateLink] URL];
GDataServiceGoogleCalendar *service = [self calendarService];
[service fetchCalendarEventByInsertingEntry:newEvent
forFeedURL:feedURL
delegate:self
didFinishSelector:@selector(addTicket:addedEntry:)
didFailSelector:@selector(addTicket:failedWithError:)];
}GData services always return the newest version of an object that has been inserted or updated, so the methods are called "fetch" even for inserts and updates.
Adding custom data to GDataObject instances
Often it is useful to add data locally to a GDataObject. For example, an entry used to represent a photo being uploaded would be more convenient if it also carried a path to the photo's file.
Your application can add data to any instance of a GDataObject (such as entry and feed objects, as well as individual elements) in three ways.
Each GDataObject has methods setUserData: and userData to set and retrieve a single NSObject.
An application can set and retrieve objects as named properties of any GDataObject instance with the methods setProperty:forKey: and propertyForKey:. Note that property names beginning with an underscore are reserved by the library and should not be used by applications.
Finally, applications may subclass GDataObjects to add fields and methods. To have your subclasses be instantiated in place of the standard object class during the parsing of XML following a fetch, call setServiceSurrogates:, as demonstrated here:
NSDictionary *surrogates = [NSDictionary dictionaryWithObjectsAndKeys: [MyEntryPhoto class], [GDataEntryPhoto class], [MyEntryAlbum class], [GDataEntryPhotoAlbum class], nil]; service = [[GDataServiceGooglePicasaWeb alloc] init]; [service setServiceSurrogates:surrogates];
These three techniques only add data to elements locally for the Objective-C code; the data will not be retained on the server. Some services support an extendedProperty element which can retain arbitrary data for users.
To retain data from a fetch call to its success or failure callback, use GDataServiceTicket's setUserData: method.
Uploading files
Some services allow uploading of a file when inserting an entry into a feed. Uploading requires setting the upload data and MIME type in the entry. Some services also require a slug as the file name.
This snippet shows the basic steps for uploading a spreadsheet document.
GDataEntrySpreadsheetDoc *newEntry = [GDataEntrySpreadsheetDoc documentEntry];
NSString *path = @"/mySpreadsheet.xls";
NSData *data = [NSData dataWithContentsOfFile:path];
if (data) {
NSString *fileName = [path lastPathComponent];
[newEntry setUploadSlug:filename];
[newEntry setUploadData:data];
[newEntry setUploadMIMEType:@"application/vnd.ms-excel"];
NSString *title = [[NSFileManager defaultManager] displayNameAtPath:path];
[newEntry setTitleWithString:title];
NSURL *postURL = [[[docListFeed links] postLink] URL];
ticket = [service fetchDocEntryByInsertingEntry:newEntry
forFeedURL:postURL
delegate:self
didFinishSelector:finishedSel
didFailSelector:failedSel];
}Upload progress monitoring
When uploading large blocks of data, such as photos or videos, your application can request periodic callbacks to update a progress indicator. To receive the callback, set an upload progress selector in the service, such as:
SEL progressSel = @selector(inputStream:hasDeliveredByteCount:ofTotalByteCount:); [service setServiceUploadProgressSelector:progressSel]; // then do the fetch // GDataServiceTicket *ticket = [service fetch...]; // If future tickets should not use the progress callback, // set the selector in the service back to nil [service setServiceUploadProgressSelector:nil];
The callback is a method with a signature matching this:
- (void)inputStream:(GDataProgressMonitorInputStream *)stream
hasDeliveredByteCount:(unsigned long long)numberOfBytesRead
ofTotalByteCount:(unsigned long long)dataLength {
// The progress method can obtain the ticket by
// calling [stream monitorSource];
}Status 304 and service data caching
GData servers provide a "Last-Modified" header with their responses. The service object remembers the header, and provides it as an "If-Modified-Since" request the next time the application makes a request to the same URL. If the request would have the same response as it did previously, the server returns no data to the second request, just status 304, "Not modified".
Your service delegate will see the "Not modified" response in its ticket:failedWithError: method. The application handles it like this:
- (void)ticket:(GDataServiceTicket *)ticket failedWithError:(NSError *)error {
if ([error code] == kGDataHTTPFetcherStatusNotModified) { // status 304
// no change since previous request
} else {
// some unexpected error occurred
}
}The service can optionally remember the dated responses in a cache and provide them to the application instead of calling the failure method. To enable the caching, the application should call
[service setShouldCacheDatedData:YES];
The service will thereafter call the delegate's ticket:finishedWithObject: method with duplicates of the original response rather than call the failure method with a status 304 error. You can call [service clearLastModifiedDates] to purge the cache, or [service setShouldCacheDatedData:NO] to purge and disable the future caching.
Fetching during modal dialogs
The networking code in GDataService classes is based on NSURLConnection, and as in NSURLConnection, callbacks are deferred while a modal dialog is displayed. Under Mac OS X 10.5 or later, you can specify run loop modes to allow networking callbacks during modal dialogs:
NSArray *modes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSModalPanelRunLoopMode, nil]; [service setRunLoopModes:modes];
Setting the run loop modes will have no effect for service objects on Mac OS X 10.4.
Authentication errors and captchas
Authenticating the user with Google's services is handled mostly transparently by the framework. If your application sends the setUserCredentialsWithUsername:password: message to the service object, the service object will sign in prior to fetching the requested object.
The most common authentication error is an invalid username or password. Occasionally, the servers may also request that the user solve a captcha, a visual puzzle.
It is optional for your application to handle captcha requests. One easy way to handle them is to just open the Google's unlock captcha web page in the user's web browser.
NSURL *captchaUnlockURL = [NSURL URLWithString:@"https://www.google.com/accounts/DisplayUnlockCaptcha"]; [[NSWorkspace sharedWorkspace] openURL:captchaUnlockURL];
For the best user experience in handling captcha requests, your application can download and display the captcha image to the user, and wait for the user to provide the answer. If the user offers an answer, put the captcha token and the user's answer into the service object with setCaptchaToken:captchaAnswer:, and retry the fetch.
To handle captchas, the fetch failed method might look something like this:
- (void)ticket:(GDataServiceTicket *)ticket failedWithError:(NSError *)error {
if ([error code] == kGDataBadAuthentication) {
NSDictionary *userInfo = [error userInfo];
NSString *authError = [userInfo authenticationError];
if ([authError isEqual:kGDataServiceErrorCaptchaRequired]) {
// URL for a captcha image (200x70 pixels)
NSURL *captchaURL = [userInfo captchaURL];
NSString *captchaToken = [userInfo captchaToken];
// a synchronous read of the image is simple, as shown here,
// but to be nice to users, you can use GDataHTTPFetcher for
// an easy asynchronous fetch of the data instead
NSData *imageData = [NSData dataWithContentsOfURL:captchaURL];
if (imageData) {
NSImage *image = [[[NSImage alloc] initWithData:imageData] autorelease];
[self askUserToSolveCaptchaImage:image
withToken:captchaToken];
// pass the token and user's captcha answer later to the service, like
// [service setCaptchaToken:captchaToken captchaAnswer:theUserAnswer]
// prior to retrying the fetch
}
} else {
// invalid username or password
}
} else {
// some other error authenticating or retrieving the GData object
// or a 304 status indicating the data has not been modified since it
// was previously fetched
}
}Tip: to force a captcha request from the server for testing, provide an invalid e-mail address as the username several times in a row.
Documentation on authentication for Google data APIs is available here.
Automatic retry of failed fetches
GData service classes and the GDataHTTPFetcher class provide a mechanism for automatic retry of a few common network and server errors, with appropriate increasing delays between each attempt. You can turn on the automatic retry support for a GData service by calling [service setIsServiceRetryEnabled:YES].
The default errors retried are http status 408 (request timeout), 503 (service unavailable), and 504 (gateway timeout), and NSURLErrorTimedOut and NSURLErrorNetworkConnectionLost. You may specify a maximum retry interval other than the default of 10 minutes, and can provide an optional retry selector to customize the criteria for each retry attempt.
Proxy Authentication
In corporate or institutional settings where a password-protected proxy is in use, a proxy error may show up in the failure callback. It would have constant error domain and code values, as shown here:
- (void)fetchTicket:(GDataServiceTicket *)ticket
failedWithError:(NSError *)error {
if ([error code] == kGDataHTTPFetcherErrorAuthenticationChallengeFailed
&& [[error domain] isEqual:kGDataHTTPFetcherErrorDomain]) {
NSURLAuthenticationChallenge *challenge;
challenge = [[error userInfo] objectForKey:kGDataHTTPFetcherErrorChallengeKey];
}
...
}If you want to handle such errors, use the challenge object to display a dialog showing information about the host and requesting account and password credentials for the proxy. The proxy credentials can then be passed to the ticket's authentication fetcher:
ticket = [service fetchFeed...];
if (proxyAccountName && proxyPassword) {
NSURLCredential *cred;
cred = [NSURLCredential credentialWithUser:proxyAccountName
password:proxyPassword
persistence:NSURLCredentialPersistencePermanent];
[[ticket authFetcher] setProxyCredential:cred];
}Logging http server traffic
Debugging GData transactions is often easier when you can browse the XML being sent back and forth over the network. To make this convenient, the framework can save copies of the server traffic, including http headers, to files in a local directory. Your application should call
[GDataHTTPFetcher setIsLoggingEnabled:YES]
to turn on logging. Normally, logs are written to the directory GDataHTTPDebugLogs in the current user's Desktop folder, though the path to another folder can be specified with the +setLoggingDirectory: method.
To view the most recently saved logs, use a web browser to open the symlink named My_App_Name_http_log_newest.html (for whatever your application's name is) in the logging directory. Note that Camino and Firefox display XML in a more useful fashion than does Safari.
Sign in to add a comment

The example applications are really helpful. I hope you guys will be releasing some iPhone example apps soon.