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