fake_drive_service.cc revision 424c4d7b64af9d0d8fd9624f381f469654d5e3d2
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/drive/fake_drive_service.h"
6
7#include <string>
8
9#include "base/file_util.h"
10#include "base/logging.h"
11#include "base/md5.h"
12#include "base/message_loop/message_loop.h"
13#include "base/strings/string_number_conversions.h"
14#include "base/strings/string_split.h"
15#include "base/strings/string_tokenizer.h"
16#include "base/strings/string_util.h"
17#include "base/strings/stringprintf.h"
18#include "base/strings/utf_string_conversions.h"
19#include "chrome/browser/google_apis/drive_api_parser.h"
20#include "chrome/browser/google_apis/gdata_wapi_parser.h"
21#include "chrome/browser/google_apis/test_util.h"
22#include "chrome/browser/google_apis/time_util.h"
23#include "content/public/browser/browser_thread.h"
24#include "net/base/escape.h"
25#include "net/base/url_util.h"
26
27using content::BrowserThread;
28using google_apis::AboutResource;
29using google_apis::AboutResourceCallback;
30using google_apis::AccountMetadata;
31using google_apis::AppList;
32using google_apis::AppListCallback;
33using google_apis::AuthStatusCallback;
34using google_apis::AuthorizeAppCallback;
35using google_apis::CancelCallback;
36using google_apis::DownloadActionCallback;
37using google_apis::EntryActionCallback;
38using google_apis::GDATA_FILE_ERROR;
39using google_apis::GDATA_NO_CONNECTION;
40using google_apis::GDATA_OTHER_ERROR;
41using google_apis::GDataErrorCode;
42using google_apis::GetContentCallback;
43using google_apis::GetResourceEntryCallback;
44using google_apis::GetResourceListCallback;
45using google_apis::GetShareUrlCallback;
46using google_apis::HTTP_BAD_REQUEST;
47using google_apis::HTTP_CREATED;
48using google_apis::HTTP_NOT_FOUND;
49using google_apis::HTTP_PRECONDITION;
50using google_apis::HTTP_RESUME_INCOMPLETE;
51using google_apis::HTTP_SUCCESS;
52using google_apis::InitiateUploadCallback;
53using google_apis::Link;
54using google_apis::ProgressCallback;
55using google_apis::ResourceEntry;
56using google_apis::ResourceList;
57using google_apis::UploadRangeCallback;
58using google_apis::UploadRangeResponse;
59namespace test_util = google_apis::test_util;
60namespace util = google_apis::util;
61
62namespace drive {
63namespace {
64
65// Rel property of an upload link in the entries dictionary value.
66const char kUploadUrlRel[] =
67    "http://schemas.google.com/g/2005#resumable-create-media";
68
69// Rel property of a share link in the entries dictionary value.
70const char kShareUrlRel[] =
71    "http://schemas.google.com/docs/2007#share";
72
73// Returns true if a resource entry matches with the search query.
74// Supports queries consist of following format.
75// - Phrases quoted by double/single quotes
76// - AND search for multiple words/phrases segmented by space
77// - Limited attribute search.  Only "title:" is supported.
78bool EntryMatchWithQuery(const ResourceEntry& entry,
79                         const std::string& query) {
80  base::StringTokenizer tokenizer(query, " ");
81  tokenizer.set_quote_chars("\"'");
82  while (tokenizer.GetNext()) {
83    std::string key, value;
84    const std::string& token = tokenizer.token();
85    if (token.find(':') == std::string::npos) {
86      TrimString(token, "\"'", &value);
87    } else {
88      base::StringTokenizer key_value(token, ":");
89      key_value.set_quote_chars("\"'");
90      if (!key_value.GetNext())
91        return false;
92      key = key_value.token();
93      if (!key_value.GetNext())
94        return false;
95      TrimString(key_value.token(), "\"'", &value);
96    }
97
98    // TODO(peria): Deal with other attributes than title.
99    if (!key.empty() && key != "title")
100      return false;
101    // Search query in the title.
102    if (entry.title().find(value) == std::string::npos)
103      return false;
104  }
105  return true;
106}
107
108// Returns |url| without query parameter.
109GURL RemoveQueryParameter(const GURL& url) {
110  GURL::Replacements replacements;
111  replacements.ClearQuery();
112  return url.ReplaceComponents(replacements);
113}
114
115void ScheduleUploadRangeCallback(const UploadRangeCallback& callback,
116                                 int64 start_position,
117                                 int64 end_position,
118                                 GDataErrorCode error,
119                                 scoped_ptr<ResourceEntry> entry) {
120  base::MessageLoop::current()->PostTask(
121      FROM_HERE,
122      base::Bind(callback,
123                 UploadRangeResponse(error,
124                                     start_position,
125                                     end_position),
126                 base::Passed(&entry)));
127}
128
129void EntryActionCallbackAdapter(
130    const EntryActionCallback& callback,
131    GDataErrorCode error, scoped_ptr<ResourceEntry> resource_entry) {
132  callback.Run(error);
133}
134
135}  // namespace
136
137struct FakeDriveService::UploadSession {
138  std::string content_type;
139  int64 content_length;
140  std::string parent_resource_id;
141  std::string resource_id;
142  std::string etag;
143  std::string title;
144
145  int64 uploaded_size;
146
147  UploadSession()
148      : content_length(0),
149        uploaded_size(0) {}
150
151  UploadSession(
152      std::string content_type,
153      int64 content_length,
154      std::string parent_resource_id,
155      std::string resource_id,
156      std::string etag,
157      std::string title)
158    : content_type(content_type),
159      content_length(content_length),
160      parent_resource_id(parent_resource_id),
161      resource_id(resource_id),
162      etag(etag),
163      title(title),
164      uploaded_size(0) {
165  }
166};
167
168FakeDriveService::FakeDriveService()
169    : largest_changestamp_(0),
170      published_date_seq_(0),
171      next_upload_sequence_number_(0),
172      default_max_results_(0),
173      resource_id_count_(0),
174      resource_list_load_count_(0),
175      change_list_load_count_(0),
176      directory_load_count_(0),
177      about_resource_load_count_(0),
178      app_list_load_count_(0),
179      offline_(false) {
180  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
181}
182
183FakeDriveService::~FakeDriveService() {
184  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
185}
186
187bool FakeDriveService::LoadResourceListForWapi(
188    const std::string& relative_path) {
189  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
190  scoped_ptr<Value> raw_value = test_util::LoadJSONFile(relative_path);
191  base::DictionaryValue* as_dict = NULL;
192  scoped_ptr<base::Value> feed;
193  base::DictionaryValue* feed_as_dict = NULL;
194
195  // Extract the "feed" from the raw value and take the ownership.
196  // Note that Remove() transfers the ownership to |feed|.
197  if (raw_value->GetAsDictionary(&as_dict) &&
198      as_dict->Remove("feed", &feed) &&
199      feed->GetAsDictionary(&feed_as_dict)) {
200    ignore_result(feed.release());
201    resource_list_value_.reset(feed_as_dict);
202
203    // Go through entries and convert test$data from a string to a binary blob.
204    base::ListValue* entries = NULL;
205    if (feed_as_dict->GetList("entry", &entries)) {
206      for (size_t i = 0; i < entries->GetSize(); ++i) {
207        base::DictionaryValue* entry = NULL;
208        std::string content_data;
209        if (entries->GetDictionary(i, &entry) &&
210            entry->GetString("test$data", &content_data)) {
211          entry->Set("test$data",
212                     base::BinaryValue::CreateWithCopiedBuffer(
213                         content_data.c_str(), content_data.size()));
214        }
215      }
216    }
217  }
218
219  return resource_list_value_;
220}
221
222bool FakeDriveService::LoadAccountMetadataForWapi(
223    const std::string& relative_path) {
224  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
225
226  // Load JSON data, which must be a dictionary.
227  scoped_ptr<base::Value> value = test_util::LoadJSONFile(relative_path);
228  CHECK_EQ(base::Value::TYPE_DICTIONARY, value->GetType());
229  account_metadata_value_.reset(
230      static_cast<base::DictionaryValue*>(value.release()));
231
232  // Update the largest_changestamp_.
233  scoped_ptr<AccountMetadata> account_metadata =
234      AccountMetadata::CreateFrom(*account_metadata_value_);
235  largest_changestamp_ = account_metadata->largest_changestamp();
236
237  // Add the largest changestamp to the existing entries.
238  // This will be used to generate change lists in GetResourceList().
239  if (resource_list_value_) {
240    base::ListValue* entries = NULL;
241    if (resource_list_value_->GetList("entry", &entries)) {
242      for (size_t i = 0; i < entries->GetSize(); ++i) {
243        base::DictionaryValue* entry = NULL;
244        if (entries->GetDictionary(i, &entry)) {
245          entry->SetString("docs$changestamp.value",
246                           base::Int64ToString(largest_changestamp_));
247        }
248      }
249    }
250  }
251
252  return account_metadata_value_;
253}
254
255bool FakeDriveService::LoadAppListForDriveApi(
256    const std::string& relative_path) {
257  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
258  app_info_value_ = test_util::LoadJSONFile(relative_path);
259  return app_info_value_;
260}
261
262void FakeDriveService::SetQuotaValue(int64 used, int64 total) {
263  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
264  DCHECK(account_metadata_value_);
265
266  account_metadata_value_->SetString("entry.gd$quotaBytesUsed.$t",
267                                     base::Int64ToString16(used));
268  account_metadata_value_->SetString("entry.gd$quotaBytesTotal.$t",
269                                     base::Int64ToString16(total));
270}
271
272GURL FakeDriveService::GetFakeLinkUrl(const std::string& resource_id) {
273  return GURL("https://fake_server/" + net::EscapePath(resource_id));
274}
275
276void FakeDriveService::Initialize() {
277  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
278}
279
280void FakeDriveService::AddObserver(DriveServiceObserver* observer) {
281  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
282}
283
284void FakeDriveService::RemoveObserver(DriveServiceObserver* observer) {
285  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
286}
287
288bool FakeDriveService::CanSendRequest() const {
289  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
290  return true;
291}
292
293std::string FakeDriveService::CanonicalizeResourceId(
294    const std::string& resource_id) const {
295  return resource_id;
296}
297
298bool FakeDriveService::HasAccessToken() const {
299  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
300  return true;
301}
302
303void FakeDriveService::RequestAccessToken(const AuthStatusCallback& callback) {
304  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
305  DCHECK(!callback.is_null());
306  callback.Run(google_apis::HTTP_NOT_MODIFIED, "fake_access_token");
307}
308
309bool FakeDriveService::HasRefreshToken() const {
310  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
311  return true;
312}
313
314void FakeDriveService::ClearAccessToken() {
315  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
316}
317
318void FakeDriveService::ClearRefreshToken() {
319  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
320}
321
322std::string FakeDriveService::GetRootResourceId() const {
323  return "fake_root";
324}
325
326CancelCallback FakeDriveService::GetAllResourceList(
327    const GetResourceListCallback& callback) {
328  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
329  DCHECK(!callback.is_null());
330
331  GetResourceListInternal(0,  // start changestamp
332                          std::string(),  // empty search query
333                          std::string(),  // no directory resource id,
334                          0,  // start offset
335                          default_max_results_,
336                          &resource_list_load_count_,
337                          callback);
338  return CancelCallback();
339}
340
341CancelCallback FakeDriveService::GetResourceListInDirectory(
342    const std::string& directory_resource_id,
343    const GetResourceListCallback& callback) {
344  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
345  DCHECK(!directory_resource_id.empty());
346  DCHECK(!callback.is_null());
347
348  GetResourceListInternal(0,  // start changestamp
349                          std::string(),  // empty search query
350                          directory_resource_id,
351                          0,  // start offset
352                          default_max_results_,
353                          &directory_load_count_,
354                          callback);
355  return CancelCallback();
356}
357
358CancelCallback FakeDriveService::Search(
359    const std::string& search_query,
360    const GetResourceListCallback& callback) {
361  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
362  DCHECK(!search_query.empty());
363  DCHECK(!callback.is_null());
364
365  GetResourceListInternal(0,  // start changestamp
366                          search_query,
367                          std::string(),  // no directory resource id,
368                          0,  // start offset
369                          default_max_results_,
370                          NULL,
371                          callback);
372  return CancelCallback();
373}
374
375CancelCallback FakeDriveService::SearchByTitle(
376    const std::string& title,
377    const std::string& directory_resource_id,
378    const GetResourceListCallback& callback) {
379  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
380  DCHECK(!title.empty());
381  DCHECK(!callback.is_null());
382
383  // Note: the search implementation here doesn't support quotation unescape,
384  // so don't escape here.
385  GetResourceListInternal(0,  // start changestamp
386                          base::StringPrintf("title:'%s'", title.c_str()),
387                          directory_resource_id,
388                          0,  // start offset
389                          default_max_results_,
390                          NULL,
391                          callback);
392  return CancelCallback();
393}
394
395CancelCallback FakeDriveService::GetChangeList(
396    int64 start_changestamp,
397    const GetResourceListCallback& callback) {
398  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
399  DCHECK(!callback.is_null());
400
401  GetResourceListInternal(start_changestamp,
402                          std::string(),  // empty search query
403                          std::string(),  // no directory resource id,
404                          0,  // start offset
405                          default_max_results_,
406                          &change_list_load_count_,
407                          callback);
408  return CancelCallback();
409}
410
411CancelCallback FakeDriveService::ContinueGetResourceList(
412    const GURL& override_url,
413    const GetResourceListCallback& callback) {
414  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
415  DCHECK(!override_url.is_empty());
416  DCHECK(!callback.is_null());
417
418  // "changestamp", "q", "parent" and "start-offset" are parameters to
419  // implement "paging" of the result on FakeDriveService.
420  // The URL should be the one filled in GetResourceListInternal of the
421  // previous method invocation, so it should start with "http://localhost/?".
422  // See also GetResourceListInternal.
423  DCHECK_EQ(override_url.host(), "localhost");
424  DCHECK_EQ(override_url.path(), "/");
425
426  int64 start_changestamp = 0;
427  std::string search_query;
428  std::string directory_resource_id;
429  int start_offset = 0;
430  int max_results = default_max_results_;
431  std::vector<std::pair<std::string, std::string> > parameters;
432  if (base::SplitStringIntoKeyValuePairs(
433          override_url.query(), '=', '&', &parameters)) {
434    for (size_t i = 0; i < parameters.size(); ++i) {
435      if (parameters[i].first == "changestamp") {
436        base::StringToInt64(parameters[i].second, &start_changestamp);
437      } else if (parameters[i].first == "q") {
438        search_query =
439            net::UnescapeURLComponent(parameters[i].second,
440                                      net::UnescapeRule::URL_SPECIAL_CHARS);
441      } else if (parameters[i].first == "parent") {
442        directory_resource_id =
443            net::UnescapeURLComponent(parameters[i].second,
444                                      net::UnescapeRule::URL_SPECIAL_CHARS);
445      } else if (parameters[i].first == "start-offset") {
446        base::StringToInt(parameters[i].second, &start_offset);
447      } else if (parameters[i].first == "max-results") {
448        base::StringToInt(parameters[i].second, &max_results);
449      }
450    }
451  }
452
453  GetResourceListInternal(
454      start_changestamp, search_query, directory_resource_id,
455      start_offset, max_results, NULL, callback);
456  return CancelCallback();
457}
458
459CancelCallback FakeDriveService::GetRemainingChangeList(
460    const std::string& page_token,
461    const GetResourceListCallback& callback) {
462  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
463  DCHECK(!page_token.empty());
464  DCHECK(!callback.is_null());
465
466  return ContinueGetResourceList(GURL(page_token), callback);
467}
468
469CancelCallback FakeDriveService::GetRemainingFileList(
470    const std::string& page_token,
471    const GetResourceListCallback& callback) {
472  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
473  DCHECK(!page_token.empty());
474  DCHECK(!callback.is_null());
475
476  return ContinueGetResourceList(GURL(page_token), callback);
477}
478
479CancelCallback FakeDriveService::GetResourceEntry(
480    const std::string& resource_id,
481    const GetResourceEntryCallback& callback) {
482  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
483  DCHECK(!callback.is_null());
484
485  if (offline_) {
486    scoped_ptr<ResourceEntry> null;
487    base::MessageLoop::current()->PostTask(
488        FROM_HERE,
489        base::Bind(callback,
490                   GDATA_NO_CONNECTION,
491                   base::Passed(&null)));
492    return CancelCallback();
493  }
494
495  base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
496  if (entry) {
497    scoped_ptr<ResourceEntry> resource_entry =
498        ResourceEntry::CreateFrom(*entry);
499    base::MessageLoop::current()->PostTask(
500        FROM_HERE,
501        base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry)));
502    return CancelCallback();
503  }
504
505  scoped_ptr<ResourceEntry> null;
506  base::MessageLoop::current()->PostTask(
507      FROM_HERE,
508      base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
509  return CancelCallback();
510}
511
512CancelCallback FakeDriveService::GetShareUrl(
513    const std::string& resource_id,
514    const GURL& /* embed_origin */,
515    const GetShareUrlCallback& callback) {
516  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
517  DCHECK(!callback.is_null());
518
519  if (offline_) {
520    scoped_ptr<ResourceEntry> null;
521    base::MessageLoop::current()->PostTask(
522        FROM_HERE,
523        base::Bind(callback,
524                   GDATA_NO_CONNECTION,
525                   GURL()));
526    return CancelCallback();
527  }
528
529  base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
530  if (entry) {
531    // Share urls are stored in the resource entry, and they do not rely on the
532    // embedding origin.
533    scoped_ptr<ResourceEntry> resource_entry =
534        ResourceEntry::CreateFrom(*entry);
535    const Link* share_url = resource_entry->GetLinkByType(Link::LINK_SHARE);
536    if (share_url) {
537      base::MessageLoop::current()->PostTask(
538          FROM_HERE,
539          base::Bind(callback, HTTP_SUCCESS, share_url->href()));
540    } else {
541      base::MessageLoop::current()->PostTask(
542          FROM_HERE,
543          base::Bind(callback, HTTP_SUCCESS, GURL()));
544    }
545    return CancelCallback();
546  }
547
548  scoped_ptr<ResourceEntry> null;
549  base::MessageLoop::current()->PostTask(
550      FROM_HERE,
551      base::Bind(callback, HTTP_NOT_FOUND, GURL()));
552  return CancelCallback();
553}
554
555CancelCallback FakeDriveService::GetAboutResource(
556    const AboutResourceCallback& callback) {
557  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
558  DCHECK(!callback.is_null());
559
560  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
561  DCHECK(!callback.is_null());
562
563  if (offline_) {
564    scoped_ptr<AboutResource> null;
565    base::MessageLoop::current()->PostTask(
566        FROM_HERE,
567        base::Bind(callback,
568                   GDATA_NO_CONNECTION, base::Passed(&null)));
569    return CancelCallback();
570  }
571
572  ++about_resource_load_count_;
573  scoped_ptr<AboutResource> about_resource(
574      AboutResource::CreateFromAccountMetadata(
575          *AccountMetadata::CreateFrom(*account_metadata_value_),
576          GetRootResourceId()));
577  // Overwrite the change id.
578  about_resource->set_largest_change_id(largest_changestamp_);
579  base::MessageLoop::current()->PostTask(
580      FROM_HERE,
581      base::Bind(callback,
582                 HTTP_SUCCESS, base::Passed(&about_resource)));
583  return CancelCallback();
584}
585
586CancelCallback FakeDriveService::GetAppList(const AppListCallback& callback) {
587  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
588  DCHECK(!callback.is_null());
589  DCHECK(app_info_value_);
590
591  if (offline_) {
592    scoped_ptr<AppList> null;
593    base::MessageLoop::current()->PostTask(
594        FROM_HERE,
595        base::Bind(callback,
596                   GDATA_NO_CONNECTION,
597                   base::Passed(&null)));
598    return CancelCallback();
599  }
600
601  ++app_list_load_count_;
602  scoped_ptr<AppList> app_list(AppList::CreateFrom(*app_info_value_));
603  base::MessageLoop::current()->PostTask(
604      FROM_HERE,
605      base::Bind(callback, HTTP_SUCCESS, base::Passed(&app_list)));
606  return CancelCallback();
607}
608
609CancelCallback FakeDriveService::DeleteResource(
610    const std::string& resource_id,
611    const std::string& etag,
612    const EntryActionCallback& callback) {
613  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
614  DCHECK(!callback.is_null());
615
616  if (offline_) {
617    base::MessageLoop::current()->PostTask(
618        FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
619    return CancelCallback();
620  }
621
622  base::ListValue* entries = NULL;
623  // Go through entries and remove the one that matches |resource_id|.
624  if (resource_list_value_->GetList("entry", &entries)) {
625    for (size_t i = 0; i < entries->GetSize(); ++i) {
626      base::DictionaryValue* entry = NULL;
627      std::string current_resource_id;
628      if (entries->GetDictionary(i, &entry) &&
629          entry->GetString("gd$resourceId.$t", &current_resource_id) &&
630          resource_id == current_resource_id) {
631        GDataErrorCode error = google_apis::GDATA_OTHER_ERROR;
632        if (entry->HasKey("gd$deleted")) {
633          error = HTTP_NOT_FOUND;
634        } else {
635          entry->Set("gd$deleted", new DictionaryValue);
636          AddNewChangestampAndETag(entry);
637          error = HTTP_SUCCESS;
638        }
639        base::MessageLoop::current()->PostTask(
640            FROM_HERE, base::Bind(callback, error));
641        return CancelCallback();
642      }
643    }
644  }
645
646  base::MessageLoop::current()->PostTask(
647      FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
648  return CancelCallback();
649}
650
651CancelCallback FakeDriveService::DownloadFile(
652    const base::FilePath& local_cache_path,
653    const std::string& resource_id,
654    const DownloadActionCallback& download_action_callback,
655    const GetContentCallback& get_content_callback,
656    const ProgressCallback& progress_callback) {
657  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
658  DCHECK(!download_action_callback.is_null());
659
660  if (offline_) {
661    base::MessageLoop::current()->PostTask(
662        FROM_HERE,
663        base::Bind(download_action_callback,
664                   GDATA_NO_CONNECTION,
665                   base::FilePath()));
666    return CancelCallback();
667  }
668
669  // The field content.src is the URL to download the file.
670  base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
671  if (!entry) {
672    base::MessageLoopProxy::current()->PostTask(
673        FROM_HERE,
674        base::Bind(download_action_callback, HTTP_NOT_FOUND, base::FilePath()));
675    return CancelCallback();
676  }
677
678  // Write "x"s of the file size specified in the entry.
679  std::string file_size_string;
680  entry->GetString("docs$size.$t", &file_size_string);
681  int64 file_size = 0;
682  if (base::StringToInt64(file_size_string, &file_size)) {
683    base::BinaryValue* content_binary_data;
684    std::string content_data;
685    if (entry->GetBinary("test$data", &content_binary_data)) {
686      content_data = std::string(content_binary_data->GetBuffer(),
687          content_binary_data->GetSize());
688    }
689    DCHECK_EQ(static_cast<size_t>(file_size), content_data.size());
690
691    if (!get_content_callback.is_null()) {
692      const int64 kBlockSize = 5;
693      for (int64 i = 0; i < file_size; i += kBlockSize) {
694        const int64 size = std::min(kBlockSize, file_size - i);
695        scoped_ptr<std::string> content_for_callback(
696            new std::string(content_data.substr(i, size)));
697        base::MessageLoopProxy::current()->PostTask(
698            FROM_HERE,
699            base::Bind(get_content_callback, HTTP_SUCCESS,
700                       base::Passed(&content_for_callback)));
701      }
702    }
703
704    if (test_util::WriteStringToFile(local_cache_path, content_data)) {
705      if (!progress_callback.is_null()) {
706        // See also the comment in ResumeUpload(). For testing that clients
707        // can handle the case progress_callback is called multiple times,
708        // here we invoke the callback twice.
709        base::MessageLoopProxy::current()->PostTask(
710            FROM_HERE,
711            base::Bind(progress_callback, file_size / 2, file_size));
712        base::MessageLoopProxy::current()->PostTask(
713            FROM_HERE,
714            base::Bind(progress_callback, file_size, file_size));
715      }
716      base::MessageLoopProxy::current()->PostTask(
717          FROM_HERE,
718          base::Bind(download_action_callback,
719                     HTTP_SUCCESS,
720                     local_cache_path));
721      return CancelCallback();
722    }
723  }
724
725  // Failed to write the content.
726  base::MessageLoopProxy::current()->PostTask(
727      FROM_HERE,
728      base::Bind(download_action_callback, GDATA_FILE_ERROR, base::FilePath()));
729  return CancelCallback();
730}
731
732CancelCallback FakeDriveService::CopyResource(
733    const std::string& resource_id,
734    const std::string& in_parent_resource_id,
735    const std::string& new_title,
736    const GetResourceEntryCallback& callback) {
737  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
738  DCHECK(!callback.is_null());
739
740  if (offline_) {
741    scoped_ptr<ResourceEntry> null;
742    base::MessageLoop::current()->PostTask(
743        FROM_HERE,
744        base::Bind(callback,
745                   GDATA_NO_CONNECTION,
746                   base::Passed(&null)));
747    return CancelCallback();
748  }
749
750  const std::string& parent_resource_id = in_parent_resource_id.empty() ?
751      GetRootResourceId() : in_parent_resource_id;
752
753  base::ListValue* entries = NULL;
754  // Go through entries and copy the one that matches |resource_id|.
755  if (resource_list_value_->GetList("entry", &entries)) {
756    for (size_t i = 0; i < entries->GetSize(); ++i) {
757      base::DictionaryValue* entry = NULL;
758      std::string current_resource_id;
759      if (entries->GetDictionary(i, &entry) &&
760          entry->GetString("gd$resourceId.$t", &current_resource_id) &&
761          resource_id == current_resource_id) {
762        // Make a copy and set the new resource ID and the new title.
763        scoped_ptr<DictionaryValue> copied_entry(entry->DeepCopy());
764        copied_entry->SetString("gd$resourceId.$t",
765                                resource_id + "_copied");
766        copied_entry->SetString("title.$t", new_title);
767
768        // Reset parent directory.
769        base::ListValue* links = NULL;
770        if (!copied_entry->GetList("link", &links)) {
771          links = new base::ListValue;
772          copied_entry->Set("link", links);
773        }
774        links->Clear();
775
776        base::DictionaryValue* link = new base::DictionaryValue;
777        link->SetString(
778            "rel", "http://schemas.google.com/docs/2007#parent");
779        link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec());
780        links->Append(link);
781
782        AddNewChangestampAndETag(copied_entry.get());
783
784        // Parse the new entry.
785        scoped_ptr<ResourceEntry> resource_entry =
786            ResourceEntry::CreateFrom(*copied_entry);
787        // Add it to the resource list.
788        entries->Append(copied_entry.release());
789
790        base::MessageLoop::current()->PostTask(
791            FROM_HERE,
792            base::Bind(callback,
793                       HTTP_SUCCESS,
794                       base::Passed(&resource_entry)));
795        return CancelCallback();
796      }
797    }
798  }
799
800  scoped_ptr<ResourceEntry> null;
801  base::MessageLoop::current()->PostTask(
802      FROM_HERE,
803      base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
804  return CancelCallback();
805}
806
807CancelCallback FakeDriveService::CopyHostedDocument(
808    const std::string& resource_id,
809    const std::string& new_title,
810    const GetResourceEntryCallback& callback) {
811  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
812  DCHECK(!callback.is_null());
813
814  return CopyResource(resource_id, std::string(), new_title, callback);
815}
816
817CancelCallback FakeDriveService::MoveResource(
818    const std::string& resource_id,
819    const std::string& parent_resource_id,
820    const std::string& new_title,
821    const google_apis::GetResourceEntryCallback& callback) {
822  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
823  DCHECK(!callback.is_null());
824
825  if (offline_) {
826    base::MessageLoop::current()->PostTask(
827        FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION,
828                              base::Passed(scoped_ptr<ResourceEntry>())));
829    return CancelCallback();
830  }
831
832  base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
833  if (entry) {
834    entry->SetString("title.$t", new_title);
835
836    // Set parent if necessary.
837    if (!parent_resource_id.empty()) {
838      base::ListValue* links = NULL;
839      if (!entry->GetList("link", &links)) {
840        links = new base::ListValue;
841        entry->Set("link", links);
842      }
843
844      // Remove old parent(s).
845      for (size_t i = 0; i < links->GetSize(); ) {
846        base::DictionaryValue* link = NULL;
847        std::string rel;
848        std::string href;
849        if (links->GetDictionary(i, &link) &&
850            link->GetString("rel", &rel) &&
851            link->GetString("href", &href) &&
852            rel == "http://schemas.google.com/docs/2007#parent") {
853          links->Remove(i, NULL);
854        } else {
855          ++i;
856        }
857      }
858
859      base::DictionaryValue* link = new base::DictionaryValue;
860      link->SetString("rel", "http://schemas.google.com/docs/2007#parent");
861      link->SetString(
862          "href", GetFakeLinkUrl(parent_resource_id).spec());
863      links->Append(link);
864    }
865
866    AddNewChangestampAndETag(entry);
867
868    // Parse the new entry.
869    scoped_ptr<ResourceEntry> resource_entry =
870        ResourceEntry::CreateFrom(*entry);
871    base::MessageLoop::current()->PostTask(
872        FROM_HERE,
873        base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry)));
874    return CancelCallback();
875  }
876
877  base::MessageLoop::current()->PostTask(
878      FROM_HERE,
879      base::Bind(callback, HTTP_NOT_FOUND,
880                 base::Passed(scoped_ptr<ResourceEntry>())));
881  return CancelCallback();
882}
883
884CancelCallback FakeDriveService::RenameResource(
885    const std::string& resource_id,
886    const std::string& new_title,
887    const EntryActionCallback& callback) {
888  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
889  DCHECK(!callback.is_null());
890
891  return MoveResource(
892      resource_id, std::string(), new_title,
893      base::Bind(&EntryActionCallbackAdapter, callback));
894}
895
896CancelCallback FakeDriveService::TouchResource(
897    const std::string& resource_id,
898    const base::Time& modified_date,
899    const base::Time& last_viewed_by_me_date,
900    const GetResourceEntryCallback& callback) {
901  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
902  DCHECK(!modified_date.is_null());
903  DCHECK(!last_viewed_by_me_date.is_null());
904  DCHECK(!callback.is_null());
905
906  if (offline_) {
907    base::MessageLoop::current()->PostTask(
908        FROM_HERE,
909        base::Bind(callback, GDATA_NO_CONNECTION,
910                   base::Passed(scoped_ptr<ResourceEntry>())));
911    return CancelCallback();
912  }
913
914  base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
915  if (!entry) {
916    base::MessageLoop::current()->PostTask(
917        FROM_HERE,
918        base::Bind(callback, HTTP_NOT_FOUND,
919                   base::Passed(scoped_ptr<ResourceEntry>())));
920    return CancelCallback();
921  }
922
923  entry->SetString("updated.$t",
924                   util::FormatTimeAsString(modified_date));
925  entry->SetString("gd$lastViewed.$t",
926                   util::FormatTimeAsString(last_viewed_by_me_date));
927  AddNewChangestampAndETag(entry);
928
929  scoped_ptr<ResourceEntry> parsed_entry(ResourceEntry::CreateFrom(*entry));
930  base::MessageLoop::current()->PostTask(
931      FROM_HERE,
932      base::Bind(callback, HTTP_SUCCESS, base::Passed(&parsed_entry)));
933  return CancelCallback();
934}
935
936CancelCallback FakeDriveService::AddResourceToDirectory(
937    const std::string& parent_resource_id,
938    const std::string& resource_id,
939    const EntryActionCallback& callback) {
940  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
941  DCHECK(!callback.is_null());
942
943  if (offline_) {
944    base::MessageLoop::current()->PostTask(
945        FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
946    return CancelCallback();
947  }
948
949  base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
950  if (entry) {
951    base::ListValue* links = NULL;
952    if (!entry->GetList("link", &links)) {
953      links = new base::ListValue;
954      entry->Set("link", links);
955    }
956
957    // On the real Drive server, resources do not necessary shape a tree
958    // structure. That is, each resource can have multiple parent.
959    // We mimic the behavior here; AddResourceToDirectoy just adds
960    // one more parent link, not overwriting old links.
961    base::DictionaryValue* link = new base::DictionaryValue;
962    link->SetString("rel", "http://schemas.google.com/docs/2007#parent");
963    link->SetString(
964        "href", GetFakeLinkUrl(parent_resource_id).spec());
965    links->Append(link);
966
967    AddNewChangestampAndETag(entry);
968    base::MessageLoop::current()->PostTask(
969        FROM_HERE, base::Bind(callback, HTTP_SUCCESS));
970    return CancelCallback();
971  }
972
973  base::MessageLoop::current()->PostTask(
974      FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
975  return CancelCallback();
976}
977
978CancelCallback FakeDriveService::RemoveResourceFromDirectory(
979    const std::string& parent_resource_id,
980    const std::string& resource_id,
981    const EntryActionCallback& callback) {
982  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
983  DCHECK(!callback.is_null());
984
985  if (offline_) {
986    base::MessageLoop::current()->PostTask(
987        FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
988    return CancelCallback();
989  }
990
991  base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
992  if (entry) {
993    base::ListValue* links = NULL;
994    if (entry->GetList("link", &links)) {
995      GURL parent_content_url = GetFakeLinkUrl(parent_resource_id);
996      for (size_t i = 0; i < links->GetSize(); ++i) {
997        base::DictionaryValue* link = NULL;
998        std::string rel;
999        std::string href;
1000        if (links->GetDictionary(i, &link) &&
1001            link->GetString("rel", &rel) &&
1002            link->GetString("href", &href) &&
1003            rel == "http://schemas.google.com/docs/2007#parent" &&
1004            GURL(href) == parent_content_url) {
1005          links->Remove(i, NULL);
1006          AddNewChangestampAndETag(entry);
1007          base::MessageLoop::current()->PostTask(
1008              FROM_HERE, base::Bind(callback, HTTP_SUCCESS));
1009          return CancelCallback();
1010        }
1011      }
1012    }
1013  }
1014
1015  base::MessageLoop::current()->PostTask(
1016      FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
1017  return CancelCallback();
1018}
1019
1020CancelCallback FakeDriveService::AddNewDirectory(
1021    const std::string& parent_resource_id,
1022    const std::string& directory_title,
1023    const GetResourceEntryCallback& callback) {
1024  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1025  DCHECK(!callback.is_null());
1026
1027  if (offline_) {
1028    scoped_ptr<ResourceEntry> null;
1029    base::MessageLoop::current()->PostTask(
1030        FROM_HERE,
1031        base::Bind(callback,
1032                   GDATA_NO_CONNECTION,
1033                   base::Passed(&null)));
1034    return CancelCallback();
1035  }
1036
1037  const char kContentType[] = "application/atom+xml;type=feed";
1038  const base::DictionaryValue* new_entry = AddNewEntry(kContentType,
1039                                                       "",  // content_data
1040                                                       parent_resource_id,
1041                                                       directory_title,
1042                                                       false,  // shared_with_me
1043                                                       "folder");
1044  if (!new_entry) {
1045    scoped_ptr<ResourceEntry> null;
1046    base::MessageLoop::current()->PostTask(
1047        FROM_HERE,
1048        base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
1049    return CancelCallback();
1050  }
1051
1052  scoped_ptr<ResourceEntry> parsed_entry(ResourceEntry::CreateFrom(*new_entry));
1053  base::MessageLoop::current()->PostTask(
1054      FROM_HERE,
1055      base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry)));
1056  return CancelCallback();
1057}
1058
1059CancelCallback FakeDriveService::InitiateUploadNewFile(
1060    const std::string& content_type,
1061    int64 content_length,
1062    const std::string& parent_resource_id,
1063    const std::string& title,
1064    const InitiateUploadCallback& callback) {
1065  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1066  DCHECK(!callback.is_null());
1067
1068  if (offline_) {
1069    base::MessageLoop::current()->PostTask(
1070        FROM_HERE,
1071        base::Bind(callback, GDATA_NO_CONNECTION, GURL()));
1072    return CancelCallback();
1073  }
1074
1075  if (parent_resource_id != GetRootResourceId() &&
1076      !FindEntryByResourceId(parent_resource_id)) {
1077    base::MessageLoop::current()->PostTask(
1078        FROM_HERE,
1079        base::Bind(callback, HTTP_NOT_FOUND, GURL()));
1080    return CancelCallback();
1081  }
1082
1083  GURL session_url = GetNewUploadSessionUrl();
1084  upload_sessions_[session_url] =
1085      UploadSession(content_type, content_length,
1086                    parent_resource_id,
1087                    "",  // resource_id
1088                    "",  // etag
1089                    title);
1090
1091  base::MessageLoop::current()->PostTask(
1092      FROM_HERE,
1093      base::Bind(callback, HTTP_SUCCESS, session_url));
1094  return CancelCallback();
1095}
1096
1097CancelCallback FakeDriveService::InitiateUploadExistingFile(
1098    const std::string& content_type,
1099    int64 content_length,
1100    const std::string& resource_id,
1101    const std::string& etag,
1102    const InitiateUploadCallback& callback) {
1103  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1104  DCHECK(!callback.is_null());
1105
1106  if (offline_) {
1107    base::MessageLoop::current()->PostTask(
1108        FROM_HERE,
1109        base::Bind(callback, GDATA_NO_CONNECTION, GURL()));
1110    return CancelCallback();
1111  }
1112
1113  DictionaryValue* entry = FindEntryByResourceId(resource_id);
1114  if (!entry) {
1115    base::MessageLoop::current()->PostTask(
1116        FROM_HERE,
1117        base::Bind(callback, HTTP_NOT_FOUND, GURL()));
1118    return CancelCallback();
1119  }
1120
1121  std::string entry_etag;
1122  entry->GetString("gd$etag", &entry_etag);
1123  if (!etag.empty() && etag != entry_etag) {
1124    base::MessageLoop::current()->PostTask(
1125        FROM_HERE,
1126        base::Bind(callback, HTTP_PRECONDITION, GURL()));
1127    return CancelCallback();
1128  }
1129
1130  GURL session_url = GetNewUploadSessionUrl();
1131  upload_sessions_[session_url] =
1132      UploadSession(content_type, content_length,
1133                    "",  // parent_resource_id
1134                    resource_id,
1135                    entry_etag,
1136                    "" /* title */);
1137
1138  base::MessageLoop::current()->PostTask(
1139      FROM_HERE,
1140      base::Bind(callback, HTTP_SUCCESS, session_url));
1141  return CancelCallback();
1142}
1143
1144CancelCallback FakeDriveService::GetUploadStatus(
1145    const GURL& upload_url,
1146    int64 content_length,
1147    const UploadRangeCallback& callback) {
1148  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1149  DCHECK(!callback.is_null());
1150  return CancelCallback();
1151}
1152
1153CancelCallback FakeDriveService::ResumeUpload(
1154      const GURL& upload_url,
1155      int64 start_position,
1156      int64 end_position,
1157      int64 content_length,
1158      const std::string& content_type,
1159      const base::FilePath& local_file_path,
1160      const UploadRangeCallback& callback,
1161      const ProgressCallback& progress_callback) {
1162  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1163  DCHECK(!callback.is_null());
1164
1165  GetResourceEntryCallback completion_callback
1166      = base::Bind(&ScheduleUploadRangeCallback,
1167                   callback, start_position, end_position);
1168
1169  if (offline_) {
1170    completion_callback.Run(GDATA_NO_CONNECTION, scoped_ptr<ResourceEntry>());
1171    return CancelCallback();
1172  }
1173
1174  if (!upload_sessions_.count(upload_url)) {
1175    completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
1176    return CancelCallback();
1177  }
1178
1179  UploadSession* session = &upload_sessions_[upload_url];
1180
1181  // Chunks are required to be sent in such a ways that they fill from the start
1182  // of the not-yet-uploaded part with no gaps nor overlaps.
1183  if (session->uploaded_size != start_position) {
1184    completion_callback.Run(HTTP_BAD_REQUEST, scoped_ptr<ResourceEntry>());
1185    return CancelCallback();
1186  }
1187
1188  if (!progress_callback.is_null()) {
1189    // In the real GDataWapi/Drive DriveService, progress is reported in
1190    // nondeterministic timing. In this fake implementation, we choose to call
1191    // it twice per one ResumeUpload. This is for making sure that client code
1192    // works fine even if the callback is invoked more than once; it is the
1193    // crucial difference of the progress callback from others.
1194    // Note that progress is notified in the relative offset in each chunk.
1195    const int64 chunk_size = end_position - start_position;
1196    base::MessageLoop::current()->PostTask(
1197        FROM_HERE, base::Bind(progress_callback, chunk_size / 2, chunk_size));
1198    base::MessageLoop::current()->PostTask(
1199        FROM_HERE, base::Bind(progress_callback, chunk_size, chunk_size));
1200  }
1201
1202  if (content_length != end_position) {
1203    session->uploaded_size = end_position;
1204    completion_callback.Run(HTTP_RESUME_INCOMPLETE,
1205                            scoped_ptr<ResourceEntry>());
1206    return CancelCallback();
1207  }
1208
1209  std::string content_data;
1210  if (!file_util::ReadFileToString(local_file_path, &content_data)) {
1211    session->uploaded_size = end_position;
1212    completion_callback.Run(GDATA_FILE_ERROR, scoped_ptr<ResourceEntry>());
1213    return CancelCallback();
1214  }
1215  session->uploaded_size = end_position;
1216
1217  // |resource_id| is empty if the upload is for new file.
1218  if (session->resource_id.empty()) {
1219    DCHECK(!session->parent_resource_id.empty());
1220    DCHECK(!session->title.empty());
1221    const DictionaryValue* new_entry = AddNewEntry(
1222        session->content_type,
1223        content_data,
1224        session->parent_resource_id,
1225        session->title,
1226        false,  // shared_with_me
1227        "file");
1228    if (!new_entry) {
1229      completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
1230      return CancelCallback();
1231    }
1232
1233    completion_callback.Run(HTTP_CREATED,
1234                            ResourceEntry::CreateFrom(*new_entry));
1235    return CancelCallback();
1236  }
1237
1238  DictionaryValue* entry = FindEntryByResourceId(session->resource_id);
1239  if (!entry) {
1240    completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
1241    return CancelCallback();
1242  }
1243
1244  std::string entry_etag;
1245  entry->GetString("gd$etag", &entry_etag);
1246  if (entry_etag.empty() || session->etag != entry_etag) {
1247    completion_callback.Run(HTTP_PRECONDITION, scoped_ptr<ResourceEntry>());
1248    return CancelCallback();
1249  }
1250
1251  entry->SetString("docs$md5Checksum.$t", base::MD5String(content_data));
1252  entry->Set("test$data",
1253             base::BinaryValue::CreateWithCopiedBuffer(
1254                 content_data.data(), content_data.size()));
1255  entry->SetString("docs$size.$t", base::Int64ToString(end_position));
1256  AddNewChangestampAndETag(entry);
1257
1258  completion_callback.Run(HTTP_SUCCESS, ResourceEntry::CreateFrom(*entry));
1259  return CancelCallback();
1260}
1261
1262CancelCallback FakeDriveService::AuthorizeApp(
1263    const std::string& resource_id,
1264    const std::string& app_id,
1265    const AuthorizeAppCallback& callback) {
1266  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1267  DCHECK(!callback.is_null());
1268  return CancelCallback();
1269}
1270
1271void FakeDriveService::AddNewFile(const std::string& content_type,
1272                                  const std::string& content_data,
1273                                  const std::string& parent_resource_id,
1274                                  const std::string& title,
1275                                  bool shared_with_me,
1276                                  const GetResourceEntryCallback& callback) {
1277  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1278  DCHECK(!callback.is_null());
1279
1280  if (offline_) {
1281    scoped_ptr<ResourceEntry> null;
1282    base::MessageLoop::current()->PostTask(
1283        FROM_HERE,
1284        base::Bind(callback,
1285                   GDATA_NO_CONNECTION,
1286                   base::Passed(&null)));
1287    return;
1288  }
1289
1290  // Prepare "kind" for hosted documents. This only supports Google Document.
1291  std::string entry_kind;
1292  if (content_type == "application/vnd.google-apps.document")
1293    entry_kind = "document";
1294  else
1295    entry_kind = "file";
1296
1297  const base::DictionaryValue* new_entry = AddNewEntry(content_type,
1298                                                       content_data,
1299                                                       parent_resource_id,
1300                                                       title,
1301                                                       shared_with_me,
1302                                                       entry_kind);
1303  if (!new_entry) {
1304    scoped_ptr<ResourceEntry> null;
1305    base::MessageLoop::current()->PostTask(
1306        FROM_HERE,
1307        base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
1308    return;
1309  }
1310
1311  scoped_ptr<ResourceEntry> parsed_entry(
1312      ResourceEntry::CreateFrom(*new_entry));
1313  base::MessageLoop::current()->PostTask(
1314      FROM_HERE,
1315      base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry)));
1316}
1317
1318void FakeDriveService::SetLastModifiedTime(
1319    const std::string& resource_id,
1320    const base::Time& last_modified_time,
1321    const GetResourceEntryCallback& callback) {
1322  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1323  DCHECK(!callback.is_null());
1324
1325  if (offline_) {
1326    scoped_ptr<ResourceEntry> null;
1327    base::MessageLoop::current()->PostTask(
1328        FROM_HERE,
1329        base::Bind(callback,
1330                   GDATA_NO_CONNECTION,
1331                   base::Passed(&null)));
1332    return;
1333  }
1334
1335  base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
1336  if (!entry) {
1337    scoped_ptr<ResourceEntry> null;
1338    base::MessageLoop::current()->PostTask(
1339        FROM_HERE,
1340        base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
1341    return;
1342  }
1343
1344  if (last_modified_time.is_null()) {
1345    entry->Remove("updated.$t", NULL);
1346  } else {
1347    entry->SetString("updated.$t",
1348                     util::FormatTimeAsString(last_modified_time));
1349  }
1350
1351  scoped_ptr<ResourceEntry> parsed_entry(
1352      ResourceEntry::CreateFrom(*entry));
1353  base::MessageLoop::current()->PostTask(
1354      FROM_HERE,
1355      base::Bind(callback, HTTP_SUCCESS, base::Passed(&parsed_entry)));
1356}
1357
1358base::DictionaryValue* FakeDriveService::FindEntryByResourceId(
1359    const std::string& resource_id) {
1360  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1361
1362  base::ListValue* entries = NULL;
1363  // Go through entries and return the one that matches |resource_id|.
1364  if (resource_list_value_->GetList("entry", &entries)) {
1365    for (size_t i = 0; i < entries->GetSize(); ++i) {
1366      base::DictionaryValue* entry = NULL;
1367      std::string current_resource_id;
1368      if (entries->GetDictionary(i, &entry) &&
1369          entry->GetString("gd$resourceId.$t", &current_resource_id) &&
1370          resource_id == current_resource_id) {
1371        return entry;
1372      }
1373    }
1374  }
1375
1376  return NULL;
1377}
1378
1379base::DictionaryValue* FakeDriveService::FindEntryByContentUrl(
1380    const GURL& content_url) {
1381  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1382
1383  base::ListValue* entries = NULL;
1384  // Go through entries and return the one that matches |content_url|.
1385  if (resource_list_value_->GetList("entry", &entries)) {
1386    for (size_t i = 0; i < entries->GetSize(); ++i) {
1387      base::DictionaryValue* entry = NULL;
1388      std::string current_content_url;
1389      if (entries->GetDictionary(i, &entry) &&
1390          entry->GetString("content.src", &current_content_url) &&
1391          content_url == GURL(current_content_url)) {
1392        return entry;
1393      }
1394    }
1395  }
1396
1397  return NULL;
1398}
1399
1400std::string FakeDriveService::GetNewResourceId() {
1401  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1402
1403  ++resource_id_count_;
1404  return base::StringPrintf("resource_id_%d", resource_id_count_);
1405}
1406
1407void FakeDriveService::AddNewChangestampAndETag(base::DictionaryValue* entry) {
1408  ++largest_changestamp_;
1409  entry->SetString("docs$changestamp.value",
1410                   base::Int64ToString(largest_changestamp_));
1411  entry->SetString("gd$etag",
1412                   "etag_" + base::Int64ToString(largest_changestamp_));
1413}
1414
1415const base::DictionaryValue* FakeDriveService::AddNewEntry(
1416    const std::string& content_type,
1417    const std::string& content_data,
1418    const std::string& parent_resource_id,
1419    const std::string& title,
1420    bool shared_with_me,
1421    const std::string& entry_kind) {
1422  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
1423
1424  if (parent_resource_id != GetRootResourceId() &&
1425      !FindEntryByResourceId(parent_resource_id)) {
1426    return NULL;
1427  }
1428
1429  std::string resource_id = GetNewResourceId();
1430  GURL upload_url = GURL("https://xxx/upload/" + resource_id);
1431
1432  scoped_ptr<base::DictionaryValue> new_entry(new base::DictionaryValue);
1433  // Set the resource ID and the title
1434  new_entry->SetString("gd$resourceId.$t", resource_id);
1435  new_entry->SetString("title.$t", title);
1436  new_entry->SetString("docs$filename", title);
1437  // Set the contents, size and MD5 for a file.
1438  if (entry_kind == "file") {
1439    new_entry->Set("test$data",
1440        base::BinaryValue::CreateWithCopiedBuffer(
1441            content_data.c_str(), content_data.size()));
1442    new_entry->SetString("docs$size.$t",
1443                         base::Int64ToString(content_data.size()));
1444    new_entry->SetString("docs$md5Checksum.$t",
1445                         base::MD5String(content_data));
1446  }
1447
1448  // Add "category" which sets the resource type to |entry_kind|.
1449  base::ListValue* categories = new base::ListValue;
1450  base::DictionaryValue* category = new base::DictionaryValue;
1451  category->SetString("scheme", "http://schemas.google.com/g/2005#kind");
1452  category->SetString("term", "http://schemas.google.com/docs/2007#" +
1453                      entry_kind);
1454  categories->Append(category);
1455  new_entry->Set("category", categories);
1456  if (shared_with_me) {
1457    base::DictionaryValue* shared_with_me_label = new base::DictionaryValue;
1458    shared_with_me_label->SetString("label", "shared-with-me");
1459    shared_with_me_label->SetString("scheme",
1460                                    "http://schemas.google.com/g/2005/labels");
1461    shared_with_me_label->SetString(
1462        "term", "http://schemas.google.com/g/2005/labels#shared");
1463    categories->Append(shared_with_me_label);
1464  }
1465
1466  std::string escaped_resource_id = net::EscapePath(resource_id);
1467
1468  // Add "content" which sets the content URL.
1469  base::DictionaryValue* content = new base::DictionaryValue;
1470  content->SetString("src", "https://xxx/content/" + escaped_resource_id);
1471  content->SetString("type", content_type);
1472  new_entry->Set("content", content);
1473
1474  // Add "link" which sets the parent URL, the edit URL and the upload URL.
1475  base::ListValue* links = new base::ListValue;
1476
1477  base::DictionaryValue* parent_link = new base::DictionaryValue;
1478  parent_link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec());
1479  parent_link->SetString("rel",
1480                         "http://schemas.google.com/docs/2007#parent");
1481  links->Append(parent_link);
1482
1483  base::DictionaryValue* edit_link = new base::DictionaryValue;
1484  edit_link->SetString("href", "https://xxx/edit/" + escaped_resource_id);
1485  edit_link->SetString("rel", "edit");
1486  links->Append(edit_link);
1487
1488  base::DictionaryValue* upload_link = new base::DictionaryValue;
1489  upload_link->SetString("href", upload_url.spec());
1490  upload_link->SetString("rel", kUploadUrlRel);
1491  links->Append(upload_link);
1492
1493  const GURL share_url = net::AppendOrReplaceQueryParameter(
1494      share_url_base_, "name", title);
1495  base::DictionaryValue* share_link = new base::DictionaryValue;
1496  upload_link->SetString("href", share_url.spec());
1497  upload_link->SetString("rel", kShareUrlRel);
1498  links->Append(share_link);
1499  new_entry->Set("link", links);
1500
1501  AddNewChangestampAndETag(new_entry.get());
1502
1503  base::Time published_date =
1504      base::Time() + base::TimeDelta::FromMilliseconds(++published_date_seq_);
1505  new_entry->SetString("published.$t",
1506                       util::FormatTimeAsString(published_date));
1507
1508  // If there are no entries, prepare an empty entry to add.
1509  if (!resource_list_value_->HasKey("entry"))
1510    resource_list_value_->Set("entry", new ListValue);
1511
1512  base::DictionaryValue* raw_new_entry = new_entry.release();
1513  base::ListValue* entries = NULL;
1514  if (resource_list_value_->GetList("entry", &entries))
1515    entries->Append(raw_new_entry);
1516
1517  return raw_new_entry;
1518}
1519
1520void FakeDriveService::GetResourceListInternal(
1521    int64 start_changestamp,
1522    const std::string& search_query,
1523    const std::string& directory_resource_id,
1524    int start_offset,
1525    int max_results,
1526    int* load_counter,
1527    const GetResourceListCallback& callback) {
1528  if (offline_) {
1529    base::MessageLoop::current()->PostTask(
1530        FROM_HERE,
1531        base::Bind(callback,
1532                   GDATA_NO_CONNECTION,
1533                   base::Passed(scoped_ptr<ResourceList>())));
1534    return;
1535  }
1536
1537  scoped_ptr<ResourceList> resource_list =
1538      ResourceList::CreateFrom(*resource_list_value_);
1539
1540  // TODO(hashimoto): Drive API always provides largest changestamp. Remove this
1541  // if-statement after API switch.
1542  if (start_changestamp > 0 && start_offset == 0)
1543    resource_list->set_largest_changestamp(largest_changestamp_);
1544
1545  // Filter out entries per parameters like |directory_resource_id| and
1546  // |search_query|.
1547  ScopedVector<ResourceEntry>* entries = resource_list->mutable_entries();
1548
1549  int num_entries_matched = 0;
1550  for (size_t i = 0; i < entries->size();) {
1551    ResourceEntry* entry = (*entries)[i];
1552    bool should_exclude = false;
1553
1554    // If |directory_resource_id| is set, exclude the entry if it's not in
1555    // the target directory.
1556    if (!directory_resource_id.empty()) {
1557      // Get the parent resource ID of the entry.
1558      std::string parent_resource_id;
1559      const google_apis::Link* parent_link =
1560          entry->GetLinkByType(Link::LINK_PARENT);
1561      if (parent_link) {
1562        parent_resource_id =
1563            net::UnescapeURLComponent(parent_link->href().ExtractFileName(),
1564                                      net::UnescapeRule::URL_SPECIAL_CHARS);
1565      }
1566      if (directory_resource_id != parent_resource_id)
1567        should_exclude = true;
1568    }
1569
1570    // If |search_query| is set, exclude the entry if it does not contain the
1571    // search query in the title.
1572    if (!should_exclude && !search_query.empty() &&
1573        !EntryMatchWithQuery(*entry, search_query)) {
1574      should_exclude = true;
1575    }
1576
1577    // If |start_changestamp| is set, exclude the entry if the
1578    // changestamp is older than |largest_changestamp|.
1579    // See https://developers.google.com/google-apps/documents-list/
1580    // #retrieving_all_changes_since_a_given_changestamp
1581    if (start_changestamp > 0 && entry->changestamp() < start_changestamp)
1582      should_exclude = true;
1583
1584    // If the caller requests other list than change list by specifying
1585    // zero-|start_changestamp|, exclude deleted entry from the result.
1586    if (!start_changestamp && entry->deleted())
1587      should_exclude = true;
1588
1589    // The entry matched the criteria for inclusion.
1590    if (!should_exclude)
1591      ++num_entries_matched;
1592
1593    // If |start_offset| is set, exclude the entry if the entry is before the
1594    // start index. <= instead of < as |num_entries_matched| was
1595    // already incremented.
1596    if (start_offset > 0 && num_entries_matched <= start_offset)
1597      should_exclude = true;
1598
1599    if (should_exclude)
1600      entries->erase(entries->begin() + i);
1601    else
1602      ++i;
1603  }
1604
1605  // If |max_results| is set, trim the entries if the number exceeded the max
1606  // results.
1607  if (max_results > 0 && entries->size() > static_cast<size_t>(max_results)) {
1608    entries->erase(entries->begin() + max_results, entries->end());
1609    // Adds the next URL.
1610    // Here, we embed information which is needed for continuing the
1611    // GetResourceList request in the next invocation into url query
1612    // parameters.
1613    GURL next_url(base::StringPrintf(
1614        "http://localhost/?start-offset=%d&max-results=%d",
1615        start_offset + max_results,
1616        max_results));
1617    if (start_changestamp > 0) {
1618      next_url = net::AppendOrReplaceQueryParameter(
1619          next_url, "changestamp",
1620          base::Int64ToString(start_changestamp).c_str());
1621    }
1622    if (!search_query.empty()) {
1623      next_url = net::AppendOrReplaceQueryParameter(
1624          next_url, "q", search_query);
1625    }
1626    if (!directory_resource_id.empty()) {
1627      next_url = net::AppendOrReplaceQueryParameter(
1628          next_url, "parent", directory_resource_id);
1629    }
1630
1631    Link* link = new Link;
1632    link->set_type(Link::LINK_NEXT);
1633    link->set_href(next_url);
1634    resource_list->mutable_links()->push_back(link);
1635  }
1636
1637  if (load_counter)
1638    *load_counter += 1;
1639  base::MessageLoop::current()->PostTask(
1640      FROM_HERE,
1641      base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_list)));
1642}
1643
1644GURL FakeDriveService::GetNewUploadSessionUrl() {
1645  return GURL("https://upload_session_url/" +
1646              base::Int64ToString(next_upload_sequence_number_++));
1647}
1648
1649}  // namespace drive
1650