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