PageRenderTime 238ms CodeModel.GetById 2ms app.highlight 223ms RepoModel.GetById 1ms app.codeStats 1ms

/core/externals/update-engine/externals/gdata-objectivec-client/Examples/DocsSample/DocsSampleWindowController.m

http://macfuse.googlecode.com/
Objective C | 1611 lines | 1008 code | 315 blank | 288 comment | 161 complexity | 240f55a740271bde7cd9e25be55cc531 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/* Copyright (c) 2007 Google Inc.
   2 *
   3 * Licensed under the Apache License, Version 2.0 (the "License");
   4 * you may not use this file except in compliance with the License.
   5 * You may obtain a copy of the License at
   6 *
   7 *     http://www.apache.org/licenses/LICENSE-2.0
   8 *
   9 * Unless required by applicable law or agreed to in writing, software
  10 * distributed under the License is distributed on an "AS IS" BASIS,
  11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 * See the License for the specific language governing permissions and
  13 * limitations under the License.
  14 */
  15
  16//
  17//  DocsSampleWindowController.m
  18//
  19
  20//
  21// IMPORTANT:
  22//
  23// The XML-based API for Google Docs has been replaced with a more efficient
  24// and easier-to-use JSON API.  The JSON API is documented at
  25//
  26//   https://developers.google.com/drive/
  27//
  28// See the new Objective-C client library and sample code at
  29//   http://code.google.com/p/google-api-objectivec-client/
  30//
  31// This sample application and library support for the XML-based Docs
  32// API will eventually be removed.
  33//
  34
  35
  36#import "DocsSampleWindowController.h"
  37
  38#import "GData/GTMOAuth2WindowController.h"
  39
  40enum {
  41  // upload pop-up menu items
  42  kUploadAsGoogleDoc = 0,
  43  kUploadOriginal = 1,
  44  kUploadOCR = 2,
  45  kUploadDE = 3,
  46  kUploadJA = 4,
  47  kUploadEN = 5
  48};
  49
  50@interface DocsSampleWindowController (PrivateMethods)
  51- (void)updateUI;
  52- (void)updateChangeFolderPopup;
  53- (void)updateSelectedDocumentThumbnailImage;
  54- (void)imageFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error;
  55
  56- (void)fetchDocList;
  57- (void)fetchRevisionsForSelectedDoc;
  58
  59- (void)uploadFileAtPath:(NSString *)path;
  60- (void)showDownloadPanelForEntry:(GDataEntryBase *)entry suggestedTitle:(NSString *)title;
  61- (void)saveDocumentEntry:(GDataEntryBase *)docEntry toPath:(NSString *)path;
  62- (void)saveDocEntry:(GDataEntryBase *)entry toPath:(NSString *)savePath exportFormat:(NSString *)exportFormat authService:(GDataServiceGoogle *)service;
  63
  64- (GDataServiceGoogleDocs *)docsService;
  65- (GDataEntryDocBase *)selectedDoc;
  66- (GDataEntryDocRevision *)selectedRevision;
  67
  68- (GDataFeedDocList *)docListFeed;
  69- (void)setDocListFeed:(GDataFeedDocList *)feed;
  70- (NSError *)docListFetchError;
  71- (void)setDocListFetchError:(NSError *)error;
  72- (GDataServiceTicket *)docListFetchTicket;
  73- (void)setDocListFetchTicket:(GDataServiceTicket *)ticket;
  74
  75- (GDataFeedDocRevision *)revisionFeed;
  76- (void)setRevisionFeed:(GDataFeedDocRevision *)feed;
  77- (NSError *)revisionFetchError;
  78- (void)setRevisionFetchError:(NSError *)error;
  79- (GDataServiceTicket *)revisionFetchTicket;
  80- (void)setRevisionFetchTicket:(GDataServiceTicket *)ticket;
  81
  82- (GDataEntryDocListMetadata *)metadataEntry;
  83- (void)setMetadataEntry:(GDataEntryDocListMetadata *)entry;
  84
  85- (GDataServiceTicket *)uploadTicket;
  86- (void)setUploadTicket:(GDataServiceTicket *)ticket;
  87
  88- (void)displayAlert:(NSString *)title format:(NSString *)format, ...;
  89@end
  90
  91@implementation DocsSampleWindowController
  92
  93static NSString *const kKeychainItemName = @"DocsSample: Google Docs";
  94
  95static DocsSampleWindowController* gDocsSampleWindowController = nil;
  96
  97+ (DocsSampleWindowController *)sharedDocsSampleWindowController {
  98
  99  if (!gDocsSampleWindowController) {
 100    gDocsSampleWindowController = [[DocsSampleWindowController alloc] init];
 101  }
 102  return gDocsSampleWindowController;
 103}
 104
 105
 106- (id)init {
 107  return [self initWithWindowNibName:@"DocsSampleWindow"];
 108}
 109
 110- (void)awakeFromNib {
 111  // Load the OAuth token from the keychain, if it was previously saved
 112  NSString *clientID = [mClientIDField stringValue];
 113  NSString *clientSecret = [mClientSecretField stringValue];
 114
 115  GTMOAuth2Authentication *auth;
 116  auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:kKeychainItemName
 117                                                            clientID:clientID
 118                                                        clientSecret:clientSecret];
 119  [[self docsService] setAuthorizer:auth];
 120
 121  // Set the result text field to have a distinctive color and mono-spaced font
 122  // to aid in understanding of each operation.
 123  [mDocListResultTextField setTextColor:[NSColor darkGrayColor]];
 124
 125  NSFont *resultTextFont = [NSFont fontWithName:@"Monaco" size:9];
 126  [mDocListResultTextField setFont:resultTextFont];
 127
 128  [self updateUI];
 129}
 130
 131- (void)dealloc {
 132  [mDocListFeed release];
 133  [mDocListFetchTicket release];
 134  [mDocListFetchError release];
 135
 136  [mRevisionFeed release];
 137  [mRevisionFetchTicket release];
 138  [mRevisionFetchError release];
 139
 140  [mMetadataEntry release];
 141
 142  [mUploadTicket cancelTicket];
 143  [mUploadTicket release];
 144
 145  [super dealloc];
 146}
 147
 148#pragma mark -
 149
 150- (NSString *)signedInUsername {
 151  // Get the email address of the signed-in user
 152  GTMOAuth2Authentication *auth = [[self docsService] authorizer];
 153  BOOL isSignedIn = auth.canAuthorize;
 154  if (isSignedIn) {
 155    return auth.userEmail;
 156  } else {
 157    return nil;
 158  }
 159}
 160
 161- (BOOL)isSignedIn {
 162  NSString *name = [self signedInUsername];
 163  return (name != nil);
 164}
 165
 166- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel {
 167  // Applications should have client ID and client secret strings
 168  // hardcoded into the source, but the sample application asks the
 169  // developer for the strings
 170  NSString *clientID = [mClientIDField stringValue];
 171  NSString *clientSecret = [mClientSecretField stringValue];
 172
 173  if ([clientID length] == 0 || [clientSecret length] == 0) {
 174    // Remind the developer that client ID and client secret are needed
 175    [mClientIDButton performSelector:@selector(performClick:)
 176                          withObject:self
 177                          afterDelay:0.5];
 178    return;
 179  }
 180
 181  // Show the OAuth 2 sign-in controller
 182  NSString *scope = [GTMOAuth2Authentication scopeWithStrings:
 183                     [GDataServiceGoogleDocs authorizationScope],
 184                     [GDataServiceGoogleSpreadsheet authorizationScope],
 185                     nil];
 186
 187  NSBundle *frameworkBundle = [NSBundle bundleForClass:[GTMOAuth2WindowController class]];
 188  GTMOAuth2WindowController *windowController;
 189  windowController = [GTMOAuth2WindowController controllerWithScope:scope
 190                                                           clientID:clientID
 191                                                       clientSecret:clientSecret
 192                                                   keychainItemName:kKeychainItemName
 193                                                     resourceBundle:frameworkBundle];
 194  
 195  [windowController setUserData:NSStringFromSelector(signInDoneSel)];
 196  [windowController signInSheetModalForWindow:[self window]
 197                            completionHandler:^(GTMOAuth2Authentication *auth, NSError *error) {
 198                              // callback
 199                              if (error == nil) {
 200                                [[self docsService] setAuthorizer:auth];
 201
 202                                NSString *selStr = [windowController userData];
 203                                if (selStr) {
 204                                  [self performSelector:NSSelectorFromString(selStr)];
 205                                }
 206                              } else {
 207                                [self setDocListFetchError:error];
 208                                [self updateUI];
 209                              }
 210                            }];
 211}
 212
 213#pragma mark -
 214
 215- (void)updateUI {
 216  BOOL isSignedIn = [self isSignedIn];
 217  NSString *username = [self signedInUsername];
 218  [mSignedInButton setTitle:(isSignedIn ? @"Sign Out" : @"Sign In")];
 219  [mSignedInField setStringValue:(isSignedIn ? username : @"No")];
 220
 221  // docList list display
 222  [mDocListTable reloadData];
 223
 224  GDataEntryDocBase *selectedDoc = [self selectedDoc];
 225
 226  // spin indicator when retrieving feed
 227  BOOL isFetchingDocList = (mDocListFetchTicket != nil);
 228  if (isFetchingDocList) {
 229    [mDocListProgressIndicator startAnimation:self];
 230  } else {
 231    [mDocListProgressIndicator stopAnimation:self];
 232  }
 233  [mDocListCancelButton setEnabled:isFetchingDocList];
 234
 235  // show the doclist feed fetch result error or the selected entry
 236  NSString *docResultStr = @"";
 237  if (mDocListFetchError) {
 238    docResultStr = [mDocListFetchError description];
 239  } else {
 240    if (selectedDoc) {
 241      docResultStr = [selectedDoc description];
 242    }
 243  }
 244  [mDocListResultTextField setString:docResultStr];
 245
 246  [self updateSelectedDocumentThumbnailImage];
 247
 248  // revision list display
 249  [mRevisionsTable reloadData];
 250
 251  GDataEntryDocRevision *selectedRevision = [self selectedRevision];
 252
 253  // spin indicator when retrieving feed
 254  BOOL isFetchingRevisions = (mRevisionFetchTicket != nil);
 255  if (isFetchingRevisions) {
 256    [mRevisionsProgressIndicator startAnimation:self];
 257  } else {
 258    [mRevisionsProgressIndicator stopAnimation:self];
 259  }
 260  [mRevisionsCancelButton setEnabled:isFetchingRevisions];
 261
 262  // show the revision feed fetch result error or the selected entry
 263  NSString *revisionsResultStr = @"";
 264  if (mRevisionFetchError) {
 265    revisionsResultStr = [mRevisionFetchError description];
 266  } else {
 267    if (selectedRevision) {
 268      revisionsResultStr = [selectedRevision description];
 269    }
 270  }
 271  [mRevisionsResultTextField setString:revisionsResultStr];
 272
 273  BOOL isSelectedDocAStandardGDocsType =
 274    [selectedDoc isKindOfClass:[GDataEntryStandardDoc class]]
 275    || [selectedDoc isKindOfClass:[GDataEntrySpreadsheetDoc class]]
 276    || [selectedDoc isKindOfClass:[GDataEntryPresentationDoc class]];
 277
 278  // enable the button for viewing the selected doc in a browser
 279  BOOL doesDocHaveHTMLLink = ([selectedDoc HTMLLink] != nil);
 280  [mViewSelectedDocButton setEnabled:doesDocHaveHTMLLink];
 281
 282  BOOL doesRevisionHaveExportURL = ([[[selectedRevision content] sourceURI] length] > 0);
 283  [mDownloadSelectedRevisionButton setEnabled:doesRevisionHaveExportURL];
 284
 285  BOOL doesDocHaveExportURL = ([[[selectedDoc content] sourceURI] length] > 0);
 286  [mDownloadSelectedDocButton setEnabled:doesDocHaveExportURL];
 287
 288  BOOL doesDocHaveEditLink = ([selectedDoc editLink] != nil);
 289  [mDeleteSelectedDocButton setEnabled:doesDocHaveEditLink];
 290
 291  [mDuplicateSelectedDocButton setEnabled:isSelectedDocAStandardGDocsType];
 292
 293  // enable the "Show Changes" button
 294  BOOL hasFeed = (mDocListFeed != nil);
 295  [mShowChangesButton setEnabled:hasFeed];
 296
 297  // enable the publishing checkboxes when a publishable revision is selected
 298  BOOL isRevisionSelected = (selectedRevision != nil);
 299  BOOL isRevisionPublishable = isRevisionSelected
 300    && isSelectedDocAStandardGDocsType;
 301
 302  [mPublishCheckbox setEnabled:isRevisionPublishable];
 303  [mAutoRepublishCheckbox setEnabled:isRevisionPublishable];
 304  [mPublishOutsideDomainCheckbox setEnabled:isRevisionPublishable];
 305
 306  // enable the "Update Publishing" button when the selected revision is
 307  // publishable and the checkbox settings differ from the current publishing
 308  // setting for the selected revision
 309  BOOL isPublished = [[selectedRevision publish] boolValue];
 310  BOOL isPublishedChecked = ([mPublishCheckbox state] == NSOnState);
 311
 312  BOOL isAutoRepublished = [[selectedRevision publishAuto] boolValue];
 313  BOOL isAutoRepublishedChecked = ([mAutoRepublishCheckbox state] == NSOnState);
 314
 315  BOOL isExternalPublished = [[selectedRevision publishOutsideDomain] boolValue];
 316  BOOL isExternalPublishedChecked = ([mPublishOutsideDomainCheckbox state] == NSOnState);
 317
 318  BOOL canUpdatePublishing = isRevisionPublishable
 319    && ((isPublished != isPublishedChecked)
 320        || (isAutoRepublished != isAutoRepublishedChecked)
 321        || (isExternalPublished != isExternalPublishedChecked));
 322
 323  [mUpdatePublishingButton setEnabled:canUpdatePublishing];
 324
 325  // enable uploading buttons
 326  BOOL isUploading = (mUploadTicket != nil);
 327  BOOL canPostToFeed = ([mDocListFeed postLink] != nil);
 328
 329  [mUploadFileButton setEnabled:(canPostToFeed && !isUploading)];
 330  [mStopUploadButton setEnabled:isUploading];
 331  [mPauseUploadButton setEnabled:isUploading];
 332  [mCreateFolderButton setEnabled:canPostToFeed];
 333
 334  BOOL isUploadPaused = [mUploadTicket isUploadPaused];
 335  NSString *pauseTitle = (isUploadPaused ? @"Resume" : @"Pause");
 336  [mPauseUploadButton setTitle:pauseTitle];
 337
 338  // enable the "Upload Original Document" menu item only if the user metadata
 339  // indicates support for generic file uploads
 340  GDataDocFeature *feature = [mMetadataEntry featureForName:kGDataDocsFeatureNameUploadAny];
 341  BOOL canUploadGenericDocs = (feature != nil);
 342
 343  NSMenuItem *genericMenuItem = [[mUploadPopup menu] itemWithTag:kUploadOriginal];
 344  [genericMenuItem setEnabled:canUploadGenericDocs];
 345
 346  // fill in the add-to-folder pop-up for the selected doc
 347  [self updateChangeFolderPopup];
 348
 349  // show the title of the file currently uploading
 350  NSString *uploadingStr = @"";
 351  NSString *uploadingTitle = [[(GDataEntryBase *)
 352    [mDocListFetchTicket postedObject] title] stringValue];
 353
 354  if (uploadingTitle) {
 355    uploadingStr = [NSString stringWithFormat:@"Uploading: %@", uploadingTitle];
 356  }
 357  [mUploadingTextField setStringValue:uploadingStr];
 358
 359  // Show or hide the text indicating that the client ID or client secret are
 360  // needed
 361  BOOL hasClientIDStrings = [[mClientIDField stringValue] length] > 0
 362    && [[mClientSecretField stringValue] length] > 0;
 363  [mClientIDRequiredTextField setHidden:hasClientIDStrings];
 364}
 365
 366- (void)updateChangeFolderPopup {
 367
 368  // replace all menu items in the button with the folder titles and pointers
 369  // of the feed's folder entries, but preserve the pop-up's "Change Folder"
 370  // title as the first item
 371
 372  NSString *title = [mFolderMembershipPopup title];
 373
 374  NSMenu *addMenu = [[[NSMenu alloc] initWithTitle:title] autorelease];
 375  [addMenu setAutoenablesItems:NO];
 376  [addMenu addItemWithTitle:title action:nil keyEquivalent:@""];
 377  [mFolderMembershipPopup setMenu:addMenu];
 378
 379  // get all folder entries
 380  NSArray *folderEntries = [mDocListFeed entriesWithCategoryKind:kGDataCategoryFolderDoc];
 381
 382  // get hrefs of folders that already contain the selected doc
 383  GDataEntryDocBase *doc = [self selectedDoc];
 384  NSArray *parentLinks = [doc parentLinks];
 385  NSArray *parentHrefs = [parentLinks valueForKey:@"href"];
 386
 387  // disable the pop-up if a folder entry is selected
 388  BOOL isMovableDocSelected = (doc != nil)
 389    && ![doc isKindOfClass:[GDataEntryFolderDoc class]];
 390  [mFolderMembershipPopup setEnabled:isMovableDocSelected];
 391
 392  if (isMovableDocSelected) {
 393    // step through the folders in this feed, add them to the
 394    // pop-up, and add a checkmark to the names of folders that
 395    // contain the selected document
 396    NSEnumerator *folderEnum = [folderEntries objectEnumerator];
 397    GDataEntryFolderDoc *folderEntry;
 398    while ((folderEntry = [folderEnum nextObject]) != nil) {
 399
 400      NSString *title = [[folderEntry title] stringValue];
 401      NSMenuItem *item = [addMenu addItemWithTitle:title
 402                                            action:@selector(changeFolderSelected:)
 403                                     keyEquivalent:@""];
 404      [item setTarget:self];
 405      [item setRepresentedObject:folderEntry];
 406
 407      NSString *folderHref = [[folderEntry selfLink] href];
 408
 409      BOOL shouldCheckItem = (folderHref != nil)
 410        && [parentHrefs containsObject:folderHref];
 411      [item setState:shouldCheckItem];
 412    }
 413  }
 414}
 415
 416- (void)updateSelectedDocumentThumbnailImage {
 417  static NSString* priorImageURLStr = nil;
 418
 419  GDataEntryDocBase *doc = [self selectedDoc];
 420  GDataLink *thumbnailLink = [doc thumbnailLink];
 421  NSString *newImageURLStr = [thumbnailLink href];
 422
 423  if (!AreEqualOrBothNil(newImageURLStr, priorImageURLStr)) {
 424    // the image has changed
 425    priorImageURLStr = newImageURLStr;
 426
 427    [mDocListImageView setImage:nil];
 428
 429    if ([newImageURLStr length] > 0) {
 430      // We need an authorized fetcher to download the document thumbnail, as
 431      // the fetch requires authentication.
 432      //
 433      // We could attach an authorizer to the fetcher, but it's simpler to
 434      // use the GData service's fetcherService to make the new fetcher, as
 435      // that will already have the authorizer attached.
 436      GTMHTTPFetcherService *fetcherService = [[self docsService] fetcherService];
 437      GTMHTTPFetcher *fetcher = [fetcherService fetcherWithURLString:newImageURLStr];
 438      [fetcher setCommentWithFormat:@"thumbnail for \"%@\"", [[doc title] stringValue]];
 439      [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
 440        // callback
 441        if (error == nil) {
 442          NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];
 443          [mDocListImageView setImage:image];
 444        } else {
 445          NSLog(@"Error %@ loading image %@",
 446                error, [[fetcher mutableRequest] URL]);
 447        }
 448      }];
 449    }
 450  }
 451}
 452
 453- (void)displayAlert:(NSString *)title format:(NSString *)format, ... {
 454  NSString *result = format;
 455  if (format) {
 456    va_list argList;
 457    va_start(argList, format);
 458    result = [[[NSString alloc] initWithFormat:format
 459                                     arguments:argList] autorelease];
 460    va_end(argList);
 461  }
 462  NSBeginAlertSheet(title, nil, nil, nil, [self window], nil, nil,
 463                    nil, nil, @"%@", result);
 464}
 465
 466#pragma mark IBActions
 467
 468- (IBAction)signInClicked:(id)sender {
 469  if (![self isSignedIn]) {
 470    // Sign in
 471    [self runSigninThenInvokeSelector:@selector(updateUI)];
 472  } else {
 473    // Sign out
 474    GDataServiceGoogleDocs *service = [self docsService];
 475
 476    [GTMOAuth2WindowController removeAuthFromKeychainForName:kKeychainItemName];
 477    [service setAuthorizer:nil];
 478    [self updateUI];
 479  }
 480}
 481
 482- (IBAction)getDocListClicked:(id)sender {
 483  if (![self isSignedIn]) {
 484    [self runSigninThenInvokeSelector:@selector(fetchDocList)];
 485  } else {
 486    [self fetchDocList];
 487  }
 488}
 489
 490- (IBAction)cancelDocListFetchClicked:(id)sender {
 491  [mDocListFetchTicket cancelTicket];
 492  [self setDocListFetchTicket:nil];
 493  [self updateUI];
 494}
 495
 496- (IBAction)cancelRevisionsFetchClicked:(id)sender {
 497  [mRevisionFetchTicket cancelTicket];
 498  [self setRevisionFetchTicket:nil];
 499  [self updateUI];
 500}
 501
 502- (IBAction)viewSelectedDocClicked:(id)sender {
 503
 504  NSURL *docURL = [[[self selectedDoc] HTMLLink] URL];
 505
 506  if (docURL) {
 507    [[NSWorkspace sharedWorkspace] openURL:docURL];
 508  } else {
 509    NSBeep();
 510  }
 511}
 512
 513#pragma mark -
 514
 515- (IBAction)downloadSelectedDocClicked:(id)sender {
 516
 517  GDataEntryDocBase *docEntry = [self selectedDoc];
 518
 519  NSString *saveTitle = [[docEntry title] stringValue];
 520
 521  [self showDownloadPanelForEntry:docEntry
 522                   suggestedTitle:saveTitle];
 523}
 524
 525- (IBAction)downloadSelectedRevisionClicked:(id)sender {
 526
 527  GDataEntryDocRevision *revisionEntry = [self selectedRevision];
 528
 529  GDataEntryDocBase *docEntry = [self selectedDoc];
 530
 531  NSString *docName = [[docEntry title] stringValue];
 532  NSString *revisionName = [[revisionEntry title] stringValue];
 533  NSString *saveTitle = [NSString stringWithFormat:@"%@ (%@)",
 534                         docName, revisionName];
 535
 536  // the revision entry doesn't tell us the kind of document being saved, so
 537  // we'll explicitly put it into a property of the entry
 538  Class documentClass = [docEntry class];
 539  [revisionEntry setProperty:documentClass
 540                      forKey:@"document class"];
 541
 542  [self showDownloadPanelForEntry:revisionEntry
 543                   suggestedTitle:saveTitle];
 544}
 545
 546- (void)showDownloadPanelForEntry:(GDataEntryBase *)entry
 547                   suggestedTitle:(NSString *)title {
 548
 549  NSString *sourceURI = [[entry content] sourceURI];
 550  if (sourceURI) {
 551    // We will download drawings as pdf and other files as text
 552    BOOL isDrawing = [entry isKindOfClass:[GDataEntryDrawingDoc class]];
 553    NSString *filename = title;
 554    NSString *fileExtension = (isDrawing ? @"pdf" : @"txt");
 555    if (![[title pathExtension] isEqual:fileExtension]) {
 556      // The title string needs the file extension to be a file name
 557      filename = [title stringByAppendingPathExtension:fileExtension];
 558    }
 559
 560    NSSavePanel *savePanel = [NSSavePanel savePanel];
 561    [savePanel setNameFieldStringValue:filename];
 562    [savePanel beginSheetModalForWindow:[self window]
 563                      completionHandler:^(NSInteger result) {
 564                        // callback
 565                        if (result == NSOKButton) {
 566                          // user clicked OK
 567                          NSString *savePath = [[savePanel URL] path];
 568                          [self saveDocumentEntry:entry
 569                                           toPath:savePath];
 570                        }
 571                      }];
 572  } else {
 573    NSBeep();
 574  }
 575}
 576
 577// formerly saveSelectedDocumentToPath:
 578- (void)saveDocumentEntry:(GDataEntryBase *)docEntry
 579                   toPath:(NSString *)savePath {
 580  // downloading docs, per
 581  // http://code.google.com/apis/documents/docs/3.0/developers_guide_protocol.html#DownloadingDocs
 582
 583  // when downloading a revision entry, we've added a property above indicating
 584  // the class of document for which this is a revision
 585  Class classProperty = [docEntry propertyForKey:@"document class"];
 586  if (!classProperty) {
 587    classProperty = [docEntry class];
 588  }
 589
 590  // since the user has already fetched the doc list, the service object
 591  // has the proper authentication token.
 592  GDataServiceGoogleDocs *docsService = [self docsService];
 593
 594  BOOL isDrawing = [classProperty isEqual:[GDataEntryDrawingDoc class]];
 595  NSString *exportFormat = (isDrawing ? @"pdf" : @"txt");
 596  [self saveDocEntry:docEntry
 597              toPath:savePath
 598        exportFormat:exportFormat
 599         authService:docsService];
 600}
 601
 602- (void)saveDocEntry:(GDataEntryBase *)entry
 603              toPath:(NSString *)savePath
 604        exportFormat:(NSString *)exportFormat
 605         authService:(GDataServiceGoogle *)service {
 606
 607  // the content src attribute is used for downloading
 608  NSURL *exportURL = [[entry content] sourceURL];
 609  if (exportURL != nil) {
 610    // we'll use GDataQuery as a convenient way to append the exportFormat
 611    // parameter of the docs export API to the content src URL
 612    GDataQuery *query = [GDataQuery queryWithFeedURL:exportURL];
 613    [query addCustomParameterWithName:@"exportFormat"
 614                                value:exportFormat];
 615    NSURL *downloadURL = [query URL];
 616
 617    // Read the document's contents asynchronously from the network
 618
 619    // requestForURL:ETag:httpMethod: sets the user agent header of the
 620    // request and, when using ClientLogin, adds the authorization header
 621    NSURLRequest *request = [service requestForURL:downloadURL
 622                                                ETag:nil
 623                                        httpMethod:nil];
 624
 625    GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
 626    [fetcher setAuthorizer:[service authorizer]];
 627    [fetcher setDownloadPath:savePath];
 628    [fetcher setCommentWithFormat:@"downloading \"%@\"", [[entry title] stringValue]];
 629    [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
 630      // callback
 631      if (error == nil) {
 632        // Successfully saved the document
 633        //
 634        // Since a downloadPath property was specified, the data argument is
 635        // nil, and the file data has been written to disk.
 636      } else {
 637        NSLog(@"Error saving document: %@", error);
 638        NSBeep();
 639      }
 640    }];
 641  }
 642}
 643
 644/* When signing in with ClientLogin, we need to create a SpreadsheetService
 645   instance to do an authenticated download of spreadsheet documents.
 646
 647   Since this sample signs in with OAuth 2, which allows multiple scopes,
 648   we do not need to use a SpreadsheetService, but here is what it looks
 649   like for ClientLogin.
 650
 651- (void)saveSpreadsheet:(GDataEntrySpreadsheetDoc *)docEntry
 652                 toPath:(NSString *)savePath {
 653  // to download a spreadsheet document, we need a spreadsheet service object,
 654  // and we first need to fetch a feed or entry with the service object so that
 655  // it has a valid auth token
 656  GDataServiceGoogleSpreadsheet *spreadsheetService;
 657  spreadsheetService = [[[GDataServiceGoogleSpreadsheet alloc] init] autorelease];
 658
 659  GDataServiceGoogleDocs *docsService = [self docsService];
 660  [spreadsheetService setUserAgent:[docsService userAgent]];
 661  [spreadsheetService setUserCredentialsWithUsername:[docsService username]
 662                                            password:[docsService password]];
 663  GDataServiceTicket *ticket;
 664  ticket = [spreadsheetService authenticateWithDelegate:self
 665                                didAuthenticateSelector:@selector(spreadsheetTicket:authenticatedWithError:)];
 666
 667  // we'll hang on to the spreadsheet service object with a ticket property
 668  // since we need it to create an authorized NSURLRequest
 669  [ticket setProperty:docEntry forKey:@"docEntry"];
 670  [ticket setProperty:savePath forKey:@"savePath"];
 671}
 672
 673- (void)spreadsheetTicket:(GDataServiceTicket *)ticket
 674   authenticatedWithError:(NSError *)error {
 675  if (error == nil) {
 676    GDataEntrySpreadsheetDoc *docEntry = [ticket propertyForKey:@"docEntry"];
 677    NSString *savePath = [ticket propertyForKey:@"savePath"];
 678
 679    [self saveDocEntry:docEntry
 680                toPath:savePath
 681          exportFormat:@"tsv"
 682           authService:[ticket service]];
 683  } else {
 684    // failed to authenticate; give up
 685    NSLog(@"Spreadsheet authentication error: %@", error);
 686    return;
 687  }
 688}
 689*/
 690
 691#pragma mark -
 692
 693- (IBAction)uploadFileClicked:(id)sender {
 694  // ask the user to choose a file
 695  NSOpenPanel *openPanel = [NSOpenPanel openPanel];
 696  [openPanel setPrompt:@"Upload"];
 697  [openPanel beginSheetModalForWindow:[self window]
 698                    completionHandler:^(NSInteger result) {
 699                      // callback
 700                      if (result == NSOKButton) {
 701                        // user chose a file and clicked OK
 702                        //
 703                        // start uploading (deferred to the main thread since
 704                        // we currently have a sheet displayed)
 705                        NSString *path = [[openPanel URL] path];
 706                        [self performSelectorOnMainThread:@selector(uploadFileAtPath:)
 707                                               withObject:path
 708                                            waitUntilDone:NO];
 709                      }
 710                    }];
 711}
 712
 713- (IBAction)pauseUploadClicked:(id)sender {
 714  if ([mUploadTicket isUploadPaused]) {
 715    [mUploadTicket resumeUpload];
 716  } else {
 717    [mUploadTicket pauseUpload];
 718  }
 719  [self updateUI];
 720}
 721
 722- (IBAction)stopUploadClicked:(id)sender {
 723  [mUploadTicket cancelTicket];
 724  [self setUploadTicket:nil];
 725
 726  [mUploadProgressIndicator setDoubleValue:0.0];
 727  [self updateUI];
 728}
 729
 730#pragma mark -
 731
 732- (IBAction)publishCheckboxClicked:(id)sender {
 733  // enable or disable the Update Publishing button
 734  [self updateUI];
 735}
 736
 737- (IBAction)updatePublishingClicked:(id)sender {
 738  GDataServiceGoogleDocs *service = [self docsService];
 739
 740  GDataEntryDocRevision *revisionEntry = [self selectedRevision];
 741
 742  // update the revision elements to match the checkboxes
 743  //
 744  // we'll modify a copy of the selected entry so we don't leave an inaccurate
 745  // entry in the feed if our fetch fails
 746  GDataEntryDocRevision *revisionCopy = [[revisionEntry copy] autorelease];
 747
 748  BOOL shouldPublish = ([mPublishCheckbox state] == NSOnState);
 749  [revisionCopy setPublish:[NSNumber numberWithBool:shouldPublish]];
 750
 751  BOOL shouldAutoRepublish = ([mAutoRepublishCheckbox state] == NSOnState);
 752  [revisionCopy setPublishAuto:[NSNumber numberWithBool:shouldAutoRepublish]];
 753
 754  BOOL shouldPublishExternally = ([mPublishOutsideDomainCheckbox state] == NSOnState);
 755  [revisionCopy setPublishOutsideDomain:[NSNumber numberWithBool:shouldPublishExternally]];
 756
 757  [service fetchEntryByUpdatingEntry:revisionCopy
 758                   completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) {
 759                     // callback
 760                     if (error == nil) {
 761                       [self displayAlert:@"Updated"
 762                                   format:@"Updated publish status for \"%@\"",
 763                        [[entry title] stringValue]];
 764
 765                       // re-fetch the document list
 766                       [self fetchRevisionsForSelectedDoc];
 767                     } else {
 768                       [self displayAlert:@"Updated failed"
 769                                   format:@"Failed to update publish status: %@",
 770                        error];
 771                     }
 772                   }];
 773}
 774
 775#pragma mark -
 776
 777- (IBAction)createFolderClicked:(id)sender {
 778  GDataServiceGoogleDocs *service = [self docsService];
 779
 780  GDataEntryFolderDoc *docEntry = [GDataEntryFolderDoc documentEntry];
 781
 782  NSString *title = [NSString stringWithFormat:@"New Folder %@", [NSDate date]];
 783  [docEntry setTitleWithString:title];
 784
 785  NSURL *postURL = [[mDocListFeed postLink] URL];
 786
 787  [service fetchEntryByInsertingEntry:docEntry
 788                           forFeedURL:postURL
 789                    completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) {
 790                      // callback
 791                      if (error == nil) {
 792                        [self displayAlert:@"Created folder"
 793                                    format:@"Created folder \"%@\"",
 794                         [[entry title] stringValue]];
 795
 796                        // re-fetch the document list
 797                        [self fetchDocList];
 798                        [self updateUI];
 799                      } else {
 800                        [self displayAlert:@"Create failed"
 801                                    format:@"Folder create failed: %@", error];
 802                      }
 803                    }];
 804}
 805
 806#pragma mark -
 807
 808static long long gLargestPriorChangestamp = 0;
 809
 810- (IBAction)showChangesClicked:(id)sender {
 811  NSURL *changesFeedURL = [GDataServiceGoogleDocs changesFeedURLForUserID:kGDataServiceDefaultUser];
 812  GDataServiceGoogleDocs *service = [self docsService];
 813
 814  if (gLargestPriorChangestamp == 0) {
 815    // First click
 816    //
 817    // We have not previously fetched the changes feed, so request it without
 818    // entries to determine a benchmark changestamp
 819    GDataQueryDocs *query = [GDataQueryDocs documentQueryWithFeedURL:changesFeedURL];
 820
 821    // The server currently ignores zero as a max-results value (b/5027926),
 822    // so we'll request one entry and ignore it
 823    [query setMaxResults:1];
 824
 825    GDataServiceTicket *ticket;
 826    ticket = [service fetchFeedWithQuery:query
 827                       completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) {
 828                         // callback
 829                         if (error == nil) {
 830                           GDataFeedDocChange *changeFeed = (GDataFeedDocChange *)feed;
 831                           NSNumber *num = [changeFeed largestChangestamp];
 832                           [self displayAlert:@"Initial changestamp obtained"
 833                                       format:@"Value: %@", num];
 834                           gLargestPriorChangestamp = [num longLongValue];
 835                         } else {
 836                           [self displayAlert:@"Fetch failed"
 837                                       format:@"Fetch of changes failed: %@",
 838                            error];
 839                         }
 840                       }];
 841    // We don't want additional pages of this feed, since we only care about
 842    // the changestamp benchmark
 843    [ticket setShouldFollowNextLinks:NO];
 844  } else {
 845    // Second and later clicks
 846    //
 847    // We have previously fetched the changes feed, so request all changes
 848    // since that benchmark changestamp
 849    GDataQueryDocs *query = [GDataQueryDocs documentQueryWithFeedURL:changesFeedURL];
 850    [query setStartIndex:(1 + gLargestPriorChangestamp)];
 851
 852    // We'll reduce the number pages fetches needed to obtain the entire feed
 853    // by requesting a large page size. A large page size and automatic next
 854    // link following are not really practical on mobile devices, though, as
 855    // the entries of the changes feed are big.
 856    [query setMaxResults:100];
 857
 858    [service fetchFeedWithQuery:query
 859              completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) {
 860                // callback
 861                if (error == nil) {
 862                  // We obtained a feed of changes
 863                  //
 864                  // Report the titles of added and updated docs, and the
 865                  // entry identifier of removed docs
 866                  GDataFeedDocChange *changeFeed = (GDataFeedDocChange *)feed;
 867
 868                  NSMutableString *output = [NSMutableString stringWithFormat:
 869                                             @"Changed entries (%lu):",
 870                                             (unsigned long) [[feed entries] count]];
 871                  for (GDataEntryDocBase *entry in changeFeed) {
 872                    if ([entry isRemoved]) {
 873                      // Removed
 874                      [output appendFormat:@"\nRemoved (%@)", [entry identifier]];
 875                    } else {
 876                      // Added or updated
 877                      [output appendFormat:@"\n%@", [[entry title] stringValue]];
 878                    }
 879                  }
 880                  [self displayAlert:@"Changed entries"
 881                              format:@"%@", output];
 882
 883                  // Update the benchmark value
 884                  gLargestPriorChangestamp = [[changeFeed largestChangestamp] longLongValue];
 885                } else {
 886                  [self displayAlert:@"Fetch failed"
 887                              format:@"Fetch of changes since %lld failed: %@",
 888                   gLargestPriorChangestamp, error];
 889                }
 890              }];
 891  }
 892}
 893
 894#pragma mark -
 895
 896- (IBAction)deleteSelectedDocClicked:(id)sender {
 897
 898  GDataEntryDocBase *doc = [self selectedDoc];
 899  if (doc) {
 900    // make the user confirm that the selected doc should be deleted
 901    NSBeginAlertSheet(@"Delete Document", @"Delete", @"Cancel", nil,
 902                      [self window], self,
 903                      @selector(deleteDocSheetDidEnd:returnCode:contextInfo:),
 904                      nil, nil, @"Delete the document \"%@\"?",
 905                      [[doc title] stringValue]);
 906  }
 907}
 908
 909// delete dialog callback
 910- (void)deleteDocSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
 911
 912  if (returnCode == NSAlertDefaultReturn) {
 913
 914    // delete the document entry
 915    GDataEntryDocBase *entry = [self selectedDoc];
 916
 917    if (entry) {
 918      GDataServiceGoogleDocs *service = [self docsService];
 919      [service deleteEntry:entry
 920                  delegate:self
 921         didFinishSelector:@selector(deleteDocEntryTicket:deletedEntry:error:)];
 922    }
 923  }
 924}
 925
 926// entry delete callback
 927- (void)deleteDocEntryTicket:(GDataServiceTicket *)ticket
 928                deletedEntry:(GDataEntryDocBase *)object
 929                       error:(NSError *)error {
 930  if (error == nil) {
 931    // note: object is nil in the delete callback
 932    [self displayAlert:@"Deleted Doc"
 933                format:@"Document deleted"];
 934
 935    // re-fetch the document list
 936    [self fetchDocList];
 937    [self updateUI];
 938  } else {
 939    [self displayAlert:@"Delete failed"
 940                format:@"Document delete failed: %@", error];
 941  }
 942}
 943
 944#pragma mark -
 945
 946- (IBAction)duplicateSelectedDocClicked:(id)sender {
 947
 948  GDataEntryDocBase *selectedDoc = [self selectedDoc];
 949  if (selectedDoc) {
 950    // make a new entry of the same class as the selected document entry,
 951    // with just the title set and an identifier equal to the selected
 952    // doc's resource ID
 953    GDataEntryDocBase *newEntry = [[selectedDoc class] documentEntry];
 954
 955    [newEntry setIdentifier:[selectedDoc resourceID]];
 956
 957    NSString *oldTitle = [[selectedDoc title] stringValue];
 958    NSString *newTitle = [oldTitle stringByAppendingString:@" copy"];
 959    [newEntry setTitleWithString:newTitle];
 960
 961    GDataServiceGoogleDocs *service = [self docsService];
 962    NSURL *postURL = [[mDocListFeed postLink] URL];
 963
 964    [service fetchEntryByInsertingEntry:newEntry
 965                             forFeedURL:postURL
 966                      completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) {
 967                        // callback
 968                        if (error == nil) {
 969                          [self displayAlert:@"Copied Doc"
 970                                      format:@"Document duplicate \"%@\" created", [[newEntry title] stringValue]];
 971
 972                          // re-fetch the document list
 973                          [self fetchDocList];
 974                          [self updateUI];
 975                        } else {
 976                          [self displayAlert:@"Copy failed"
 977                                      format:@"Document duplicate failed: %@", error];
 978                        }
 979                      }];
 980  }
 981}
 982
 983#pragma mark -
 984
 985- (IBAction)changeFolderSelected:(id)sender {
 986
 987  // the selected menu item represents a folder; fetch the folder's feed
 988  //
 989  // with the folder's feed, we can insert or remove the selected document
 990  // entry in the folder's feed
 991
 992  GDataEntryFolderDoc *folderEntry = [sender representedObject];
 993  NSURL *folderFeedURL = [[folderEntry content] sourceURL];
 994  if (folderFeedURL != nil) {
 995
 996    GDataServiceGoogleDocs *service = [self docsService];
 997
 998    GDataServiceTicket *ticket;
 999    ticket = [service fetchFeedWithURL:folderFeedURL
1000                              delegate:self
1001                     didFinishSelector:@selector(fetchFolderTicket:finishedWithFeed:error:)];
1002
1003    // save the selected doc in the ticket's userData
1004    GDataEntryDocBase *doc = [self selectedDoc];
1005    [ticket setUserData:doc];
1006  }
1007}
1008
1009// folder feed fetch callback
1010- (void)fetchFolderTicket:(GDataServiceTicket *)ticket
1011         finishedWithFeed:(GDataFeedDocList *)feed
1012                    error:(NSError *)error {
1013
1014  if (error == nil) {
1015    GDataEntryDocBase *docEntry = [ticket userData];
1016
1017    GDataServiceGoogleDocs *service = [self docsService];
1018    GDataServiceTicket *ticket2;
1019
1020    // if the entry is not in the folder's feed, insert it; otherwise, delete
1021    // it from the folder's feed
1022    GDataEntryDocBase *foundEntry = [feed entryForIdentifier:[docEntry identifier]];
1023    if (foundEntry == nil) {
1024      // the doc isn't currently in this folder's feed
1025      //
1026      // post the doc to the folder's feed
1027      NSURL *postURL = [[feed postLink] URL];
1028
1029      ticket2 = [service fetchEntryByInsertingEntry:docEntry
1030                                         forFeedURL:postURL
1031                                  completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) {
1032                                    // callback
1033                                    if (error == nil) {
1034                                      [self displayAlert:@"Added"
1035                                                  format:@"Added document \"%@\" to feed \"%@\"",
1036                                       [[entry title] stringValue],
1037                                       [[feed title] stringValue]];
1038
1039                                      // re-fetch the document list
1040                                      [self fetchDocList];
1041                                      [self updateUI];
1042                                    } else {
1043                                      [self displayAlert:@"Insert failed"
1044                                                  format:@"Insert to folder feed failed: %@", error];
1045                                    }
1046                                  }];
1047    } else {
1048      // the doc is alrady in the folder's feed, so remove it
1049      ticket2 = [service deleteEntry:foundEntry
1050                   completionHandler:^(GDataServiceTicket *ticket, id nilObject, NSError *error) {
1051                     // callback
1052                     if (error == nil) {
1053                       [self displayAlert:@"Removed"
1054                                   format:@"Removed document from feed \"%@\"", [[feed title] stringValue]];
1055
1056                       // re-fetch the document list
1057                       [self fetchDocList];
1058                       [self updateUI];
1059                     } else {
1060                       [self displayAlert:@"Fetch failed"
1061                                   format:@"Remove from folder feed failed: %@",
1062                        error];
1063                     }
1064                   }];
1065    }
1066  } else {
1067    // failed to fetch feed of folders
1068    [self displayAlert:@"Fetch failed"
1069                format:@"Fetch of folder feed failed: %@", error];
1070  }
1071}
1072
1073#pragma mark -
1074
1075- (IBAction)APIConsoleClicked:(id)sender {
1076  NSURL *url = [NSURL URLWithString:@"https://code.google.com/apis/console"];
1077  [[NSWorkspace sharedWorkspace] openURL:url];
1078}
1079
1080- (IBAction)loggingCheckboxClicked:(id)sender {
1081  [GTMHTTPFetcher setLoggingEnabled:[sender state]];
1082}
1083
1084#pragma mark -
1085
1086// get an docList service object with the current username/password
1087//
1088// A "service" object handles networking tasks.  Service objects
1089// contain user authentication information as well as networking
1090// state information (such as cookies and the "last modified" date for
1091// fetched data.)
1092
1093- (GDataServiceGoogleDocs *)docsService {
1094
1095  static GDataServiceGoogleDocs* service = nil;
1096
1097  if (!service) {
1098    service = [[GDataServiceGoogleDocs alloc] init];
1099
1100    [service setShouldCacheResponseData:YES];
1101    [service setServiceShouldFollowNextLinks:YES];
1102    [service setIsServiceRetryEnabled:YES];
1103  }
1104
1105  return service;
1106}
1107
1108// get the doc selected in the list, or nil if none
1109- (GDataEntryDocBase *)selectedDoc {
1110
1111  int rowIndex = [mDocListTable selectedRow];
1112  if (rowIndex > -1) {
1113    GDataEntryDocBase *doc = [mDocListFeed entryAtIndex:rowIndex];
1114    return doc;
1115  }
1116  return nil;
1117}
1118
1119// get the doc revision in the list, or nil if none
1120- (GDataEntryDocRevision *)selectedRevision {
1121
1122  int rowIndex = [mRevisionsTable selectedRow];
1123  if (rowIndex > -1) {
1124    GDataEntryDocRevision *entry = [mRevisionFeed entryAtIndex:rowIndex];
1125    return entry;
1126  }
1127  return nil;
1128}
1129
1130#pragma mark Fetch doc list user metadata
1131
1132- (void)fetchMetadataEntry {
1133  [self setMetadataEntry:nil];
1134
1135  NSURL *entryURL = [GDataServiceGoogleDocs metadataEntryURLForUserID:kGDataServiceDefaultUser];
1136  GDataServiceGoogleDocs *service = [self docsService];
1137  [service fetchEntryWithURL:entryURL
1138           completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) {
1139             // callback
1140             [self setMetadataEntry:(GDataEntryDocListMetadata *)entry];
1141
1142             // enable or disable features
1143             [self updateUI];
1144
1145             if (error != nil) {
1146               NSLog(@"Error fetching user metadata: %@", error);
1147             }
1148           }];
1149}
1150
1151#pragma mark Fetch doc list
1152
1153// begin retrieving the list of the user's docs
1154- (void)fetchDocList {
1155
1156  [self setDocListFeed:nil];
1157  [self setDocListFetchError:nil];
1158  [self setDocListFetchTicket:nil];
1159
1160  GDataServiceGoogleDocs *service = [self docsService];
1161  GDataServiceTicket *ticket;
1162
1163  // Fetching a feed gives us 25 responses by default.  We need to use
1164  // the feed's "next" link to get any more responses.  If we want more than 25
1165  // at a time, instead of calling fetchDocsFeedWithURL, we can create a
1166  // GDataQueryDocs object, as shown here.
1167
1168  NSURL *feedURL = [GDataServiceGoogleDocs docsFeedURL];
1169
1170  GDataQueryDocs *query = [GDataQueryDocs documentQueryWithFeedURL:feedURL];
1171  [query setMaxResults:1000];
1172  [query setShouldShowFolders:YES];
1173
1174  ticket = [service fetchFeedWithQuery:query
1175                     completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) {
1176                       // callback
1177                       [self setDocListFeed:(GDataFeedDocList *)feed];
1178                       [self setDocListFetchError:error];
1179                       [self setDocListFetchTicket:nil];
1180
1181                       [self updateUI];
1182                     }];
1183
1184  [self setDocListFetchTicket:ticket];
1185
1186  // update our metadata entry for this user
1187  [self fetchMetadataEntry];
1188
1189  [self updateUI];
1190}
1191
1192#pragma mark Fetch revisions or content feed
1193
1194- (void)fetchRevisionsForSelectedDoc {
1195
1196  [self setRevisionFeed:nil];
1197  [self setRevisionFetchError:nil];
1198  [self setRevisionFetchTicket:nil];
1199
1200  GDataEntryDocBase *selectedDoc = [self selectedDoc];
1201  GDataFeedLink *revisionFeedLink = [selectedDoc revisionFeedLink];
1202  NSURL *revisionFeedURL = [revisionFeedLink URL];
1203  if (revisionFeedURL) {
1204
1205    GDataServiceGoogleDocs *service = [self docsService];
1206    GDataServiceTicket *ticket;
1207    ticket = [service fetchFeedWithURL:revisionFeedURL
1208                     completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) {
1209                       // callback
1210                       [self setRevisionFeed:(GDataFeedDocRevision *)feed];
1211                       [self setRevisionFetchError:error];
1212                       [self setRevisionFetchTicket:nil];
1213
1214                       [self updateUI];
1215                     }];
1216
1217    [self setRevisionFetchTicket:ticket];
1218
1219  }
1220
1221  [self updateUI];
1222}
1223
1224#pragma mark Upload
1225
1226- (void)getMIMEType:(NSString **)mimeType andEntryClass:(Class *)class forExtension:(NSString *)extension {
1227
1228  // Mac OS X's UTI database doesn't know MIME types for .doc and .xls
1229  // so GDataEntryBase's MIMETypeForFileAtPath method isn't helpful here
1230
1231  struct MapEntry {
1232    NSString *extension;
1233    NSString *mimeType;
1234    NSString *className;
1235  };
1236
1237  static struct MapEntry sMap[] = {
1238    { @"csv", @"text/csv", @"GDataEntryStandardDoc" },
1239    { @"doc", @"application/msword", @"GDataEntryStandardDoc" },
1240    { @"docx", @"application/vnd.openxmlformats-officedocument.wordprocessingml.document", @"GDataEntryStandardDoc" },
1241    { @"ods", @"application/vnd.oasis.opendocument.spreadsheet", @"GDataEntrySpreadsheetDoc" },
1242    { @"odt", @"application/vnd.oasis.opendocument.text", @"GDataEntryStandardDoc" },
1243    { @"pps", @"application/vnd.ms-powerpoint", @"GDataEntryPresentationDoc" },
1244    { @"ppt", @"application/vnd.ms-powerpoint", @"GDataEntryPresentationDoc" },
1245    { @"rtf", @"application/rtf", @"GDataEntryStandardDoc" },
1246    { @"sxw", @"application/vnd.sun.xml.writer", @"GDataEntryStandardDoc" },
1247    { @"txt", @"text/plain", @"GDataEntryStandardDoc" },
1248    { @"xls", @"application/vnd.ms-excel", @"GDataEntrySpreadsheetDoc" },
1249    { @"xlsx", @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", @"GDataEntrySpreadsheetDoc" },
1250    { @"jpg", @"image/jpeg", @"GDataEntryStandardDoc" },
1251    { @"jpeg", @"image/jpeg", @"GDataEntryStandardDoc" },
1252    { @"png", @"image/png", @"GDataEntryStandardDoc" },
1253    { @"bmp", @"image/bmp", @"GDataEntryStandardDoc" },
1254    { @"gif", @"image/gif", @"GDataEntryStandardDoc" },
1255    { @"html", @"text/html", @"GDataEntryStandardDoc" },
1256    { @"htm", @"text/html", @"GDataEntryStandardDoc" },
1257    { @"tsv", @"text/tab-separated-values", @"GDataEntryStandardDoc" },
1258    { @"tab", @"text/tab-separated-values", @"GDataEntryStandardDoc" },
1259    { @"pdf", @"application/pdf", @"GDataEntryPDFDoc" },
1260    { nil, nil, nil }
1261  };
1262
1263  NSString *lowerExtn = [extension lowercaseString];
1264
1265  for (int idx = 0; sMap[idx].extension != nil; idx++) {
1266    if ([lowerExtn isEqual:sMap[idx].extension]) {
1267      *mimeType = sMap[idx].mimeType;
1268      *class = NSClassFromString(sMap[idx].className);
1269      return;
1270    }
1271  }
1272
1273  *mimeType = nil;
1274  *class = nil;
1275  return;
1276}
1277
1278- (void)uploadFileAtPath:(NSString *)path {
1279
1280  NSString *errorMsg = nil;
1281
1282  // make a new entry for the file
1283
1284  NSString *mimeType = nil;
1285  Class entryClass = nil;
1286
1287  NSString *extn = [path pathExtension];
1288  [self getMIMEType:&mimeType andEntryClass:&entryClass forExtension:extn];
1289
1290  if (!mimeType) {
1291    // for other file types, see if we can get the type from the Mac OS
1292    // and use a generic file document entry class
1293    mimeType = [GDataUtilities MIMETypeForFileAtPath:path
1294                                     defaultMIMEType:nil];
1295    entryClass = [GDataEntryFileDoc class];
1296  }
1297
1298  if (!mimeType) {
1299    errorMsg = [NSString stringWithFormat:@"need MIME type for file %@", path];
1300  }
1301
1302  if (mimeType && entryClass) {
1303
1304    GDataEntryDocBase *newEntry = [entryClass documentEntry];
1305
1306    NSString *title = [[NSFileManager defaultManager] displayNameAtPath:path];
1307    [newEntry setTitleWithString:title];
1308
1309    NSFileHandle *uploadFileHandle = [NSFileHandle fileHandleForReadingAtPath:path];
1310    if (!uploadFileHandle) {
1311      errorMsg = [NSString stringWithFormat:@"cannot read file %@", path];
1312    }
1313
1314    if (uploadFileHandle) {
1315      [newEntry setUploadFileHandle:uploadFileHandle];
1316
1317      [newEntry setUploadMIMEType:mimeType];
1318      [newEntry setUploadSlug:[path lastPathComponent]];
1319
1320      NSURL *uploadURL = [GDataServiceGoogleDocs docsUploadURL];
1321
1322      // add the OCR or translation parameters, if the user set the pop-up
1323      // button appropriately
1324      int popupTag = [[mUploadPopup selectedItem] tag];
1325      if (popupTag != 0) {
1326        NSString *targetLanguage = nil;
1327        BOOL shouldConvertToGoogleDoc = YES;
1328        BOOL shouldOCR = NO;
1329
1330        switch (popupTag) {
1331            // upload original file
1332          case kUploadOriginal: shouldConvertToGoogleDoc = NO; break;
1333
1334            // OCR
1335          case kUploadOCR:      shouldOCR = YES; break;
1336
1337            // translation
1338          case kUploadDE:       targetLanguage = @"de"; break; // german
1339          case kUploadJA:       targetLanguage = @"ja"; break; // japanese
1340          case kUploadEN:       targetLanguage = @"en"; break; // english
1341
1342          default: break;
1343        }
1344
1345        GDataQueryDocs *query = [GDataQueryDocs queryWithFeedURL:uploadURL];
1346
1347        [query setShouldConvertUpload:shouldConvertToGoogleDoc];
1348        [query setShouldOCRUpload:shouldOCR];
1349
1350        // we'll leave out the sourceLanguage parameter to get
1351        // auto-detection of the file's language
1352        //
1353        // language codes: http://www.loc.gov/standards/iso639-2/php/code_list.php
1354        [query setTargetLanguage:targetLanguage];
1355
1356        uploadURL = [query URL];
1357      }
1358
1359      // make service tickets call back into our upload progress selector
1360      GDataServiceGoogleDocs *service = [self docsService];
1361
1362      // insert the entry int

Large files files are truncated, but you can click here to view the full file