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.
5 #include "chrome/browser/google_apis/fake_drive_service.h"
9 #include "base/file_util.h"
10 #include "base/logging.h"
11 #include "base/message_loop.h"
12 #include "base/string_util.h"
13 #include "base/stringprintf.h"
14 #include "base/strings/string_number_conversions.h"
15 #include "base/strings/string_split.h"
16 #include "base/strings/string_tokenizer.h"
17 #include "base/utf_string_conversions.h"
18 #include "chrome/browser/google_apis/drive_api_parser.h"
19 #include "chrome/browser/google_apis/gdata_wapi_parser.h"
20 #include "chrome/browser/google_apis/test_util.h"
21 #include "chrome/browser/google_apis/time_util.h"
22 #include "content/public/browser/browser_thread.h"
23 #include "net/base/escape.h"
24 #include "net/base/url_util.h"
26 using content::BrowserThread
;
28 namespace google_apis
{
31 // Rel property of upload link in the entries dictionary value.
32 const char kUploadUrlRel
[] =
33 "http://schemas.google.com/g/2005#resumable-create-media";
35 // Returns true if a resource entry matches with the search query.
36 // Supports queries consist of following format.
37 // - Phrases quoted by double/single quotes
38 // - AND search for multiple words/phrases segmented by space
39 // - Limited attribute search. Only "title:" is supported.
40 bool EntryMatchWithQuery(const ResourceEntry
& entry
,
41 const std::string
& query
) {
42 base::StringTokenizer
tokenizer(query
, " ");
43 tokenizer
.set_quote_chars("\"'");
44 while (tokenizer
.GetNext()) {
45 std::string key
, value
;
46 const std::string
& token
= tokenizer
.token();
47 if (token
.find(':') == std::string::npos
) {
48 TrimString(token
, "\"'", &value
);
50 base::StringTokenizer
key_value(token
, ":");
51 key_value
.set_quote_chars("\"'");
52 if (!key_value
.GetNext())
54 key
= key_value
.token();
55 if (!key_value
.GetNext())
57 TrimString(key_value
.token(), "\"'", &value
);
60 // TODO(peria): Deal with other attributes than title.
61 if (!key
.empty() && key
!= "title")
63 // Search query in the title.
64 if (entry
.title().find(value
) == std::string::npos
)
70 // Gets the upload URL from the given entry. Returns an empty URL if not
72 GURL
GetUploadUrl(const base::DictionaryValue
& entry
) {
73 std::string upload_url
;
74 const base::ListValue
* links
= NULL
;
75 if (entry
.GetList("link", &links
) && links
) {
76 for (size_t link_index
= 0;
77 link_index
< links
->GetSize();
79 const base::DictionaryValue
* link
= NULL
;
81 if (links
->GetDictionary(link_index
, &link
) &&
82 link
&& link
->GetString("rel", &rel
) &&
83 rel
== kUploadUrlRel
&&
84 link
->GetString("href", &upload_url
)) {
89 return GURL(upload_url
);
94 FakeDriveService::FakeDriveService()
95 : largest_changestamp_(0),
96 default_max_results_(0),
97 resource_id_count_(0),
98 resource_list_load_count_(0),
99 account_metadata_load_count_(0),
100 about_resource_load_count_(0),
102 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
105 FakeDriveService::~FakeDriveService() {
106 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
109 bool FakeDriveService::LoadResourceListForWapi(
110 const std::string
& relative_path
) {
111 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
112 scoped_ptr
<Value
> raw_value
= test_util::LoadJSONFile(relative_path
);
113 base::DictionaryValue
* as_dict
= NULL
;
114 base::Value
* feed
= NULL
;
115 base::DictionaryValue
* feed_as_dict
= NULL
;
117 // Extract the "feed" from the raw value and take the ownership.
118 // Note that Remove() transfers the ownership to |feed|.
119 if (raw_value
->GetAsDictionary(&as_dict
) &&
120 as_dict
->Remove("feed", &feed
) &&
121 feed
->GetAsDictionary(&feed_as_dict
)) {
122 resource_list_value_
.reset(feed_as_dict
);
125 return resource_list_value_
;
128 bool FakeDriveService::LoadAccountMetadataForWapi(
129 const std::string
& relative_path
) {
130 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
131 account_metadata_value_
= test_util::LoadJSONFile(relative_path
);
133 // Update the largest_changestamp_.
134 scoped_ptr
<AccountMetadata
> account_metadata
=
135 AccountMetadata::CreateFrom(*account_metadata_value_
);
136 largest_changestamp_
= account_metadata
->largest_changestamp();
138 // Add the largest changestamp to the existing entries.
139 // This will be used to generate change lists in GetResourceList().
140 if (resource_list_value_
) {
141 base::DictionaryValue
* resource_list_dict
= NULL
;
142 base::ListValue
* entries
= NULL
;
143 if (resource_list_value_
->GetAsDictionary(&resource_list_dict
) &&
144 resource_list_dict
->GetList("entry", &entries
)) {
145 for (size_t i
= 0; i
< entries
->GetSize(); ++i
) {
146 base::DictionaryValue
* entry
= NULL
;
147 if (entries
->GetDictionary(i
, &entry
)) {
148 entry
->SetString("docs$changestamp.value",
149 base::Int64ToString(largest_changestamp_
));
155 return account_metadata_value_
;
158 bool FakeDriveService::LoadAppListForDriveApi(
159 const std::string
& relative_path
) {
160 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
161 app_info_value_
= test_util::LoadJSONFile(relative_path
);
162 return app_info_value_
;
165 GURL
FakeDriveService::GetFakeLinkUrl(const std::string
& resource_id
) {
166 return GURL("https://fake_server/" + resource_id
);
169 void FakeDriveService::Initialize(Profile
* profile
) {
170 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
173 void FakeDriveService::AddObserver(DriveServiceObserver
* observer
) {
174 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
177 void FakeDriveService::RemoveObserver(DriveServiceObserver
* observer
) {
178 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
181 bool FakeDriveService::CanStartOperation() const {
182 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
186 void FakeDriveService::CancelAll() {
187 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
190 bool FakeDriveService::CancelForFilePath(const base::FilePath
& file_path
) {
191 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
192 last_cancelled_file_
= file_path
;
196 OperationProgressStatusList
FakeDriveService::GetProgressStatusList() const {
197 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
198 return OperationProgressStatusList();
201 bool FakeDriveService::HasAccessToken() const {
202 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
206 bool FakeDriveService::HasRefreshToken() const {
207 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
211 void FakeDriveService::ClearAccessToken() {
212 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
215 void FakeDriveService::ClearRefreshToken() {
216 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
219 std::string
FakeDriveService::GetRootResourceId() const {
223 void FakeDriveService::GetAllResourceList(
224 const GetResourceListCallback
& callback
) {
225 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
226 DCHECK(!callback
.is_null());
228 GetResourceListInternal(0, // start changestamp
229 std::string(), // empty search query
230 std::string(), // no directory resource id,
232 default_max_results_
,
236 void FakeDriveService::GetResourceListInDirectory(
237 const std::string
& directory_resource_id
,
238 const GetResourceListCallback
& callback
) {
239 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
240 DCHECK(!directory_resource_id
.empty());
241 DCHECK(!callback
.is_null());
243 GetResourceListInternal(0, // start changestamp
244 std::string(), // empty search query
245 directory_resource_id
,
247 default_max_results_
,
251 void FakeDriveService::Search(const std::string
& search_query
,
252 const GetResourceListCallback
& callback
) {
253 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
254 DCHECK(!search_query
.empty());
255 DCHECK(!callback
.is_null());
257 GetResourceListInternal(0, // start changestamp
259 std::string(), // no directory resource id,
261 default_max_results_
,
265 void FakeDriveService::SearchInDirectory(
266 const std::string
& search_query
,
267 const std::string
& directory_resource_id
,
268 const GetResourceListCallback
& callback
) {
269 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
270 DCHECK(!search_query
.empty());
271 DCHECK(!directory_resource_id
.empty());
272 DCHECK(!callback
.is_null());
274 GetResourceListInternal(0, // start changestamp
276 directory_resource_id
,
278 default_max_results_
,
282 void FakeDriveService::GetChangeList(int64 start_changestamp
,
283 const GetResourceListCallback
& callback
) {
284 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
285 DCHECK(!callback
.is_null());
287 GetResourceListInternal(start_changestamp
,
288 std::string(), // empty search query
289 std::string(), // no directory resource id,
291 default_max_results_
,
295 void FakeDriveService::ContinueGetResourceList(
296 const GURL
& override_url
,
297 const GetResourceListCallback
& callback
) {
298 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
299 DCHECK(!override_url
.is_empty());
300 DCHECK(!callback
.is_null());
302 // "changestamp", "q", "parent" and "start-offset" are parameters to
303 // implement "paging" of the result on FakeDriveService.
304 // The URL should be the one filled in GetResourceListInternal of the
305 // previous method invocation, so it should start with "http://localhost/?".
306 // See also GetResourceListInternal.
307 DCHECK_EQ(override_url
.host(), "localhost");
308 DCHECK_EQ(override_url
.path(), "/");
310 int64 start_changestamp
= 0;
311 std::string search_query
;
312 std::string directory_resource_id
;
313 int start_offset
= 0;
314 int max_results
= default_max_results_
;
315 std::vector
<std::pair
<std::string
, std::string
> > parameters
;
316 if (base::SplitStringIntoKeyValuePairs(
317 override_url
.query(), '=', '&', ¶meters
)) {
318 for (size_t i
= 0; i
< parameters
.size(); ++i
) {
319 if (parameters
[i
].first
== "changestamp") {
320 base::StringToInt64(parameters
[i
].second
, &start_changestamp
);
321 } else if (parameters
[i
].first
== "q") {
322 search_query
= parameters
[i
].second
;
323 } else if (parameters
[i
].first
== "parent") {
324 directory_resource_id
= parameters
[i
].second
;
325 } else if (parameters
[i
].first
== "start-offset") {
326 base::StringToInt(parameters
[i
].second
, &start_offset
);
327 } else if (parameters
[i
].first
== "max-results") {
328 base::StringToInt(parameters
[i
].second
, &max_results
);
333 GetResourceListInternal(
334 start_changestamp
, search_query
, directory_resource_id
,
335 start_offset
, max_results
, callback
);
338 void FakeDriveService::GetResourceEntry(
339 const std::string
& resource_id
,
340 const GetResourceEntryCallback
& callback
) {
341 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
342 DCHECK(!callback
.is_null());
345 scoped_ptr
<ResourceEntry
> null
;
346 MessageLoop::current()->PostTask(
350 base::Passed(&null
)));
354 base::DictionaryValue
* entry
= FindEntryByResourceId(resource_id
);
356 scoped_ptr
<ResourceEntry
> resource_entry
=
357 ResourceEntry::CreateFrom(*entry
);
358 MessageLoop::current()->PostTask(
360 base::Bind(callback
, HTTP_SUCCESS
, base::Passed(&resource_entry
)));
364 scoped_ptr
<ResourceEntry
> null
;
365 MessageLoop::current()->PostTask(
367 base::Bind(callback
, HTTP_NOT_FOUND
, base::Passed(&null
)));
370 void FakeDriveService::GetAccountMetadata(
371 const GetAccountMetadataCallback
& callback
) {
372 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
373 DCHECK(!callback
.is_null());
376 scoped_ptr
<AccountMetadata
> null
;
377 MessageLoop::current()->PostTask(
381 base::Passed(&null
)));
385 ++account_metadata_load_count_
;
386 scoped_ptr
<AccountMetadata
> account_metadata
=
387 AccountMetadata::CreateFrom(*account_metadata_value_
);
388 // Overwrite the change stamp.
389 account_metadata
->set_largest_changestamp(largest_changestamp_
);
390 MessageLoop::current()->PostTask(
394 base::Passed(&account_metadata
)));
397 void FakeDriveService::GetAboutResource(
398 const GetAboutResourceCallback
& callback
) {
399 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
400 DCHECK(!callback
.is_null());
402 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
403 DCHECK(!callback
.is_null());
406 scoped_ptr
<AboutResource
> null
;
407 MessageLoop::current()->PostTask(
410 GDATA_NO_CONNECTION
, base::Passed(&null
)));
414 ++about_resource_load_count_
;
415 scoped_ptr
<AboutResource
> about_resource(
416 AboutResource::CreateFromAccountMetadata(
417 *AccountMetadata::CreateFrom(*account_metadata_value_
),
418 GetRootResourceId()));
419 // Overwrite the change id.
420 about_resource
->set_largest_change_id(largest_changestamp_
);
421 MessageLoop::current()->PostTask(
424 HTTP_SUCCESS
, base::Passed(&about_resource
)));
427 void FakeDriveService::GetAppList(const GetAppListCallback
& callback
) {
428 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
429 DCHECK(!callback
.is_null());
432 scoped_ptr
<AppList
> null
;
433 MessageLoop::current()->PostTask(
437 base::Passed(&null
)));
441 scoped_ptr
<AppList
> app_list(AppList::CreateFrom(*app_info_value_
));
442 MessageLoop::current()->PostTask(
444 base::Bind(callback
, HTTP_SUCCESS
, base::Passed(&app_list
)));
447 void FakeDriveService::DeleteResource(
448 const std::string
& resource_id
,
449 const std::string
& etag
,
450 const EntryActionCallback
& callback
) {
451 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
452 DCHECK(!callback
.is_null());
455 MessageLoop::current()->PostTask(
456 FROM_HERE
, base::Bind(callback
, GDATA_NO_CONNECTION
));
460 base::DictionaryValue
* resource_list_dict
= NULL
;
461 base::ListValue
* entries
= NULL
;
462 // Go through entries and remove the one that matches |resource_id|.
463 if (resource_list_value_
->GetAsDictionary(&resource_list_dict
) &&
464 resource_list_dict
->GetList("entry", &entries
)) {
465 for (size_t i
= 0; i
< entries
->GetSize(); ++i
) {
466 base::DictionaryValue
* entry
= NULL
;
467 std::string current_resource_id
;
468 if (entries
->GetDictionary(i
, &entry
) &&
469 entry
->GetString("gd$resourceId.$t", ¤t_resource_id
) &&
470 resource_id
== current_resource_id
) {
471 entries
->Remove(i
, NULL
);
472 MessageLoop::current()->PostTask(
473 FROM_HERE
, base::Bind(callback
, HTTP_SUCCESS
));
479 // TODO(satorux): Add support for returning "deleted" entries in
480 // changelists from GetResourceList().
481 MessageLoop::current()->PostTask(
482 FROM_HERE
, base::Bind(callback
, HTTP_NOT_FOUND
));
485 void FakeDriveService::DownloadFile(
486 const base::FilePath
& virtual_path
,
487 const base::FilePath
& local_cache_path
,
488 const GURL
& download_url
,
489 const DownloadActionCallback
& download_action_callback
,
490 const GetContentCallback
& get_content_callback
,
491 const ProgressCallback
& progress_callback
) {
492 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
493 DCHECK(!download_action_callback
.is_null());
496 MessageLoop::current()->PostTask(
498 base::Bind(download_action_callback
,
504 // The field content.src is the URL to download the file.
505 base::DictionaryValue
* entry
= FindEntryByContentUrl(download_url
);
507 base::MessageLoopProxy::current()->PostTask(
509 base::Bind(download_action_callback
, HTTP_NOT_FOUND
, base::FilePath()));
513 // Write "x"s of the file size specified in the entry.
514 std::string file_size_string
;
515 entry
->GetString("docs$size.$t", &file_size_string
);
516 // TODO(satorux): To be correct, we should update docs$md5Checksum.$t here.
518 if (base::StringToInt64(file_size_string
, &file_size
)) {
519 std::string
content(file_size
, 'x');
520 DCHECK_EQ(static_cast<size_t>(file_size
), content
.size());
522 if (!get_content_callback
.is_null()) {
523 const int64 kBlockSize
= 5;
524 for (int64 i
= 0; i
< file_size
; i
+= kBlockSize
) {
525 const int64 size
= std::min(kBlockSize
, file_size
- i
);
526 scoped_ptr
<std::string
> content_for_callback(
527 new std::string(content
.substr(i
, size
)));
528 base::MessageLoopProxy::current()->PostTask(
530 base::Bind(get_content_callback
, HTTP_SUCCESS
,
531 base::Passed(&content_for_callback
)));
535 if (static_cast<int>(content
.size()) ==
536 file_util::WriteFile(local_cache_path
,
539 if (!progress_callback
.is_null()) {
540 // See also the comment in ResumeUpload(). For testing that clients
541 // can handle the case progress_callback is called multiple times,
542 // here we invoke the callback twice.
543 base::MessageLoopProxy::current()->PostTask(
545 base::Bind(progress_callback
, file_size
/ 2, file_size
));
546 base::MessageLoopProxy::current()->PostTask(
548 base::Bind(progress_callback
, file_size
, file_size
));
550 base::MessageLoopProxy::current()->PostTask(
552 base::Bind(download_action_callback
,
559 // Failed to write the content.
560 base::MessageLoopProxy::current()->PostTask(
562 base::Bind(download_action_callback
, GDATA_FILE_ERROR
, base::FilePath()));
565 void FakeDriveService::CopyHostedDocument(
566 const std::string
& resource_id
,
567 const std::string
& new_name
,
568 const GetResourceEntryCallback
& callback
) {
569 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
570 DCHECK(!callback
.is_null());
573 scoped_ptr
<ResourceEntry
> null
;
574 MessageLoop::current()->PostTask(
578 base::Passed(&null
)));
582 base::DictionaryValue
* resource_list_dict
= NULL
;
583 base::ListValue
* entries
= NULL
;
584 // Go through entries and copy the one that matches |resource_id|.
585 if (resource_list_value_
->GetAsDictionary(&resource_list_dict
) &&
586 resource_list_dict
->GetList("entry", &entries
)) {
587 for (size_t i
= 0; i
< entries
->GetSize(); ++i
) {
588 base::DictionaryValue
* entry
= NULL
;
589 base::ListValue
* categories
= NULL
;
590 std::string current_resource_id
;
591 if (entries
->GetDictionary(i
, &entry
) &&
592 entry
->GetString("gd$resourceId.$t", ¤t_resource_id
) &&
593 resource_id
== current_resource_id
&&
594 entry
->GetList("category", &categories
)) {
595 // Check that the resource is a hosted document. We consider it a
596 // hosted document if the kind is neither "folder" nor "file".
597 for (size_t k
= 0; k
< categories
->GetSize(); ++k
) {
598 base::DictionaryValue
* category
= NULL
;
599 std::string scheme
, term
;
600 if (categories
->GetDictionary(k
, &category
) &&
601 category
->GetString("scheme", &scheme
) &&
602 category
->GetString("term", &term
) &&
603 scheme
== "http://schemas.google.com/g/2005#kind" &&
604 term
!= "http://schemas.google.com/docs/2007#file" &&
605 term
!= "http://schemas.google.com/docs/2007#folder") {
606 // Make a copy and set the new resource ID and the new title.
607 scoped_ptr
<DictionaryValue
> copied_entry(entry
->DeepCopy());
608 copied_entry
->SetString("gd$resourceId.$t",
609 resource_id
+ "_copied");
610 copied_entry
->SetString("title.$t", new_name
);
612 AddNewChangestamp(copied_entry
.get());
614 // Parse the new entry.
615 scoped_ptr
<ResourceEntry
> resource_entry
=
616 ResourceEntry::CreateFrom(*copied_entry
);
617 // Add it to the resource list.
618 entries
->Append(copied_entry
.release());
620 MessageLoop::current()->PostTask(
624 base::Passed(&resource_entry
)));
632 scoped_ptr
<ResourceEntry
> null
;
633 MessageLoop::current()->PostTask(
635 base::Bind(callback
, HTTP_NOT_FOUND
, base::Passed(&null
)));
638 void FakeDriveService::RenameResource(
639 const std::string
& resource_id
,
640 const std::string
& new_name
,
641 const EntryActionCallback
& callback
) {
642 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
643 DCHECK(!callback
.is_null());
646 MessageLoop::current()->PostTask(
647 FROM_HERE
, base::Bind(callback
, GDATA_NO_CONNECTION
));
651 base::DictionaryValue
* entry
= FindEntryByResourceId(resource_id
);
653 entry
->SetString("title.$t", new_name
);
654 AddNewChangestamp(entry
);
655 MessageLoop::current()->PostTask(
656 FROM_HERE
, base::Bind(callback
, HTTP_SUCCESS
));
660 MessageLoop::current()->PostTask(
661 FROM_HERE
, base::Bind(callback
, HTTP_NOT_FOUND
));
664 void FakeDriveService::AddResourceToDirectory(
665 const std::string
& parent_resource_id
,
666 const std::string
& resource_id
,
667 const EntryActionCallback
& callback
) {
668 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
669 DCHECK(!callback
.is_null());
672 MessageLoop::current()->PostTask(
673 FROM_HERE
, base::Bind(callback
, GDATA_NO_CONNECTION
));
677 base::DictionaryValue
* entry
= FindEntryByResourceId(resource_id
);
679 base::ListValue
* links
= NULL
;
680 if (!entry
->GetList("link", &links
)) {
681 links
= new base::ListValue
;
682 entry
->Set("link", links
);
685 // On the real Drive server, resources do not necessary shape a tree
686 // structure. That is, each resource can have multiple parent.
687 // We mimic the behavior here; AddResourceToDirectoy just adds
688 // one more parent link, not overwriting old links.
689 base::DictionaryValue
* link
= new base::DictionaryValue
;
690 link
->SetString("rel", "http://schemas.google.com/docs/2007#parent");
692 "href", GetFakeLinkUrl(parent_resource_id
).spec());
695 AddNewChangestamp(entry
);
696 MessageLoop::current()->PostTask(
697 FROM_HERE
, base::Bind(callback
, HTTP_SUCCESS
));
701 MessageLoop::current()->PostTask(
702 FROM_HERE
, base::Bind(callback
, HTTP_NOT_FOUND
));
705 void FakeDriveService::RemoveResourceFromDirectory(
706 const std::string
& parent_resource_id
,
707 const std::string
& resource_id
,
708 const EntryActionCallback
& callback
) {
709 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
710 DCHECK(!callback
.is_null());
713 MessageLoop::current()->PostTask(
714 FROM_HERE
, base::Bind(callback
, GDATA_NO_CONNECTION
));
718 base::DictionaryValue
* entry
= FindEntryByResourceId(resource_id
);
720 base::ListValue
* links
= NULL
;
721 if (entry
->GetList("link", &links
)) {
722 GURL parent_content_url
= GetFakeLinkUrl(parent_resource_id
);
723 for (size_t i
= 0; i
< links
->GetSize(); ++i
) {
724 base::DictionaryValue
* link
= NULL
;
727 if (links
->GetDictionary(i
, &link
) &&
728 link
->GetString("rel", &rel
) &&
729 link
->GetString("href", &href
) &&
730 rel
== "http://schemas.google.com/docs/2007#parent" &&
731 GURL(href
) == parent_content_url
) {
732 links
->Remove(i
, NULL
);
733 AddNewChangestamp(entry
);
734 MessageLoop::current()->PostTask(
735 FROM_HERE
, base::Bind(callback
, HTTP_SUCCESS
));
742 MessageLoop::current()->PostTask(
743 FROM_HERE
, base::Bind(callback
, HTTP_NOT_FOUND
));
746 void FakeDriveService::AddNewDirectory(
747 const std::string
& parent_resource_id
,
748 const std::string
& directory_name
,
749 const GetResourceEntryCallback
& callback
) {
750 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
751 DCHECK(!callback
.is_null());
754 scoped_ptr
<ResourceEntry
> null
;
755 MessageLoop::current()->PostTask(
759 base::Passed(&null
)));
763 const char kContentType
[] = "application/atom+xml;type=feed";
764 const base::DictionaryValue
* new_entry
= AddNewEntry(kContentType
,
770 scoped_ptr
<ResourceEntry
> null
;
771 MessageLoop::current()->PostTask(
773 base::Bind(callback
, HTTP_NOT_FOUND
, base::Passed(&null
)));
777 scoped_ptr
<ResourceEntry
> parsed_entry(ResourceEntry::CreateFrom(*new_entry
));
778 MessageLoop::current()->PostTask(
780 base::Bind(callback
, HTTP_CREATED
, base::Passed(&parsed_entry
)));
783 void FakeDriveService::InitiateUploadNewFile(
784 const base::FilePath
& drive_file_path
,
785 const std::string
& content_type
,
786 int64 content_length
,
787 const std::string
& parent_resource_id
,
788 const std::string
& title
,
789 const InitiateUploadCallback
& callback
) {
790 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
791 DCHECK(!callback
.is_null());
794 MessageLoop::current()->PostTask(
796 base::Bind(callback
, GDATA_NO_CONNECTION
, GURL()));
800 // Content length should be zero, as we'll create an empty file first. The
801 // content will be added in ResumeUpload().
802 const base::DictionaryValue
* new_entry
= AddNewEntry(content_type
,
808 MessageLoop::current()->PostTask(
810 base::Bind(callback
, HTTP_NOT_FOUND
, GURL()));
813 const GURL upload_url
= GetUploadUrl(*new_entry
);
814 DCHECK(upload_url
.is_valid());
816 MessageLoop::current()->PostTask(
818 base::Bind(callback
, HTTP_SUCCESS
, upload_url
));
821 void FakeDriveService::InitiateUploadExistingFile(
822 const base::FilePath
& drive_file_path
,
823 const std::string
& content_type
,
824 int64 content_length
,
825 const std::string
& resource_id
,
826 const std::string
& etag
,
827 const InitiateUploadCallback
& callback
) {
828 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
829 DCHECK(!callback
.is_null());
832 MessageLoop::current()->PostTask(
834 base::Bind(callback
, GDATA_NO_CONNECTION
, GURL()));
838 DictionaryValue
* entry
= FindEntryByResourceId(resource_id
);
840 MessageLoop::current()->PostTask(
842 base::Bind(callback
, HTTP_NOT_FOUND
, GURL()));
846 std::string entry_etag
;
847 entry
->GetString("gd$etag", &entry_etag
);
848 if (!etag
.empty() && etag
!= entry_etag
) {
849 MessageLoop::current()->PostTask(
851 base::Bind(callback
, HTTP_PRECONDITION
, GURL()));
854 entry
->SetString("docs$size.$t", "0");
856 const GURL upload_url
= GetUploadUrl(*entry
);
857 DCHECK(upload_url
.is_valid());
859 MessageLoop::current()->PostTask(
861 base::Bind(callback
, HTTP_SUCCESS
, upload_url
));
864 void FakeDriveService::GetUploadStatus(
865 UploadMode upload_mode
,
866 const base::FilePath
& drive_file_path
,
867 const GURL
& upload_url
,
868 int64 content_length
,
869 const UploadRangeCallback
& callback
) {
870 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
871 DCHECK(!callback
.is_null());
874 void FakeDriveService::ResumeUpload(
875 UploadMode upload_mode
,
876 const base::FilePath
& drive_file_path
,
877 const GURL
& upload_url
,
878 int64 start_position
,
880 int64 content_length
,
881 const std::string
& content_type
,
882 const scoped_refptr
<net::IOBuffer
>& buf
,
883 const UploadRangeCallback
& callback
,
884 const ProgressCallback
& progress_callback
) {
885 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
886 DCHECK(!callback
.is_null());
888 scoped_ptr
<ResourceEntry
> result_entry
;
891 MessageLoop::current()->PostTask(
894 UploadRangeResponse(GDATA_NO_CONNECTION
,
897 base::Passed(&result_entry
)));
901 DictionaryValue
* entry
= NULL
;
902 entry
= FindEntryByUploadUrl(upload_url
);
904 MessageLoop::current()->PostTask(
907 UploadRangeResponse(HTTP_NOT_FOUND
,
910 base::Passed(&result_entry
)));
914 // Chunks are required to be sent in such a ways that they fill from the start
915 // of the not-yet-uploaded part with no gaps nor overlaps.
916 std::string current_size_string
;
918 if (!entry
->GetString("docs$size.$t", ¤t_size_string
) ||
919 !base::StringToInt64(current_size_string
, ¤t_size
) ||
920 current_size
!= start_position
) {
921 MessageLoop::current()->PostTask(
924 UploadRangeResponse(HTTP_BAD_REQUEST
,
927 base::Passed(&result_entry
)));
931 entry
->SetString("docs$size.$t", base::Int64ToString(end_position
));
933 if (!progress_callback
.is_null()) {
934 // In the real GDataWapi/Drive DriveService, progress is reported in
935 // nondeterministic timing. In this fake implementation, we choose to call
936 // it twice per one ResumeUpload. This is for making sure that client code
937 // works fine even if the callback is invoked more than once; it is the
938 // crucial difference of the progress callback from others.
939 // Note that progress is notified in the relative offset in each chunk.
940 const int64 chunk_size
= end_position
- start_position
;
941 MessageLoop::current()->PostTask(
942 FROM_HERE
, base::Bind(progress_callback
, chunk_size
/ 2, chunk_size
));
943 MessageLoop::current()->PostTask(
944 FROM_HERE
, base::Bind(progress_callback
, chunk_size
, chunk_size
));
947 if (content_length
!= end_position
) {
948 MessageLoop::current()->PostTask(
951 UploadRangeResponse(HTTP_RESUME_INCOMPLETE
,
954 base::Passed(&result_entry
)));
958 result_entry
= ResourceEntry::CreateFrom(*entry
).Pass();
960 GDataErrorCode return_code
= HTTP_SUCCESS
;
961 if (upload_mode
== UPLOAD_NEW_FILE
)
962 return_code
= HTTP_CREATED
;
964 MessageLoop::current()->PostTask(
967 UploadRangeResponse(return_code
,
970 base::Passed(&result_entry
)));
973 void FakeDriveService::AuthorizeApp(const std::string
& resource_id
,
974 const std::string
& app_id
,
975 const AuthorizeAppCallback
& callback
) {
976 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
977 DCHECK(!callback
.is_null());
980 void FakeDriveService::AddNewFile(const std::string
& content_type
,
981 int64 content_length
,
982 const std::string
& parent_resource_id
,
983 const std::string
& title
,
984 const GetResourceEntryCallback
& callback
) {
985 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
986 DCHECK(!callback
.is_null());
989 scoped_ptr
<ResourceEntry
> null
;
990 MessageLoop::current()->PostTask(
994 base::Passed(&null
)));
998 const base::DictionaryValue
* new_entry
= AddNewEntry(content_type
,
1004 scoped_ptr
<ResourceEntry
> null
;
1005 MessageLoop::current()->PostTask(
1007 base::Bind(callback
, HTTP_NOT_FOUND
, base::Passed(&null
)));
1011 scoped_ptr
<ResourceEntry
> parsed_entry(
1012 ResourceEntry::CreateFrom(*new_entry
));
1013 MessageLoop::current()->PostTask(
1015 base::Bind(callback
, HTTP_CREATED
, base::Passed(&parsed_entry
)));
1018 void FakeDriveService::SetLastModifiedTime(
1019 const std::string
& resource_id
,
1020 const base::Time
& last_modified_time
,
1021 const GetResourceEntryCallback
& callback
) {
1022 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
1023 DCHECK(!callback
.is_null());
1026 scoped_ptr
<ResourceEntry
> null
;
1027 MessageLoop::current()->PostTask(
1029 base::Bind(callback
,
1030 GDATA_NO_CONNECTION
,
1031 base::Passed(&null
)));
1035 base::DictionaryValue
* entry
= FindEntryByResourceId(resource_id
);
1037 scoped_ptr
<ResourceEntry
> null
;
1038 MessageLoop::current()->PostTask(
1040 base::Bind(callback
, HTTP_NOT_FOUND
, base::Passed(&null
)));
1044 entry
->SetString("updated.$t", util::FormatTimeAsString(last_modified_time
));
1046 scoped_ptr
<ResourceEntry
> parsed_entry(
1047 ResourceEntry::CreateFrom(*entry
));
1048 MessageLoop::current()->PostTask(
1050 base::Bind(callback
, HTTP_SUCCESS
, base::Passed(&parsed_entry
)));
1053 base::DictionaryValue
* FakeDriveService::FindEntryByResourceId(
1054 const std::string
& resource_id
) {
1055 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
1057 base::DictionaryValue
* resource_list_dict
= NULL
;
1058 base::ListValue
* entries
= NULL
;
1059 // Go through entries and return the one that matches |resource_id|.
1060 if (resource_list_value_
->GetAsDictionary(&resource_list_dict
) &&
1061 resource_list_dict
->GetList("entry", &entries
)) {
1062 for (size_t i
= 0; i
< entries
->GetSize(); ++i
) {
1063 base::DictionaryValue
* entry
= NULL
;
1064 std::string current_resource_id
;
1065 if (entries
->GetDictionary(i
, &entry
) &&
1066 entry
->GetString("gd$resourceId.$t", ¤t_resource_id
) &&
1067 resource_id
== current_resource_id
) {
1076 base::DictionaryValue
* FakeDriveService::FindEntryByContentUrl(
1077 const GURL
& content_url
) {
1078 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
1080 base::DictionaryValue
* resource_list_dict
= NULL
;
1081 base::ListValue
* entries
= NULL
;
1082 // Go through entries and return the one that matches |content_url|.
1083 if (resource_list_value_
->GetAsDictionary(&resource_list_dict
) &&
1084 resource_list_dict
->GetList("entry", &entries
)) {
1085 for (size_t i
= 0; i
< entries
->GetSize(); ++i
) {
1086 base::DictionaryValue
* entry
= NULL
;
1087 std::string current_content_url
;
1088 if (entries
->GetDictionary(i
, &entry
) &&
1089 entry
->GetString("content.src", ¤t_content_url
) &&
1090 content_url
== GURL(current_content_url
)) {
1099 base::DictionaryValue
* FakeDriveService::FindEntryByUploadUrl(
1100 const GURL
& upload_url
) {
1101 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
1103 base::DictionaryValue
* resource_list_dict
= NULL
;
1104 base::ListValue
* entries
= NULL
;
1105 // Go through entries and return the one that matches |upload_url|.
1106 if (resource_list_value_
->GetAsDictionary(&resource_list_dict
) &&
1107 resource_list_dict
->GetList("entry", &entries
)) {
1108 for (size_t i
= 0; i
< entries
->GetSize(); ++i
) {
1109 base::DictionaryValue
* entry
= NULL
;
1110 base::ListValue
* links
= NULL
;
1111 if (entries
->GetDictionary(i
, &entry
) &&
1112 entry
->GetList("link", &links
) &&
1114 for (size_t link_index
= 0;
1115 link_index
< links
->GetSize();
1117 base::DictionaryValue
* link
= NULL
;
1119 std::string found_upload_url
;
1120 if (links
->GetDictionary(link_index
, &link
) &&
1121 link
&& link
->GetString("rel", &rel
) &&
1122 rel
== kUploadUrlRel
&&
1123 link
->GetString("href", &found_upload_url
) &&
1124 GURL(found_upload_url
) == upload_url
) {
1135 std::string
FakeDriveService::GetNewResourceId() {
1136 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
1138 ++resource_id_count_
;
1139 return base::StringPrintf("resource_id_%d", resource_id_count_
);
1142 void FakeDriveService::AddNewChangestamp(base::DictionaryValue
* entry
) {
1143 ++largest_changestamp_
;
1144 entry
->SetString("docs$changestamp.value",
1145 base::Int64ToString(largest_changestamp_
));
1148 const base::DictionaryValue
* FakeDriveService::AddNewEntry(
1149 const std::string
& content_type
,
1150 int64 content_length
,
1151 const std::string
& parent_resource_id
,
1152 const std::string
& title
,
1153 const std::string
& entry_kind
) {
1154 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
1156 if (parent_resource_id
!= GetRootResourceId() &&
1157 !FindEntryByResourceId(parent_resource_id
)) {
1161 base::DictionaryValue
* resource_list_dict
= NULL
;
1162 if (!resource_list_value_
->GetAsDictionary(&resource_list_dict
))
1165 std::string resource_id
= GetNewResourceId();
1166 GURL upload_url
= GURL("https://xxx/upload/" + resource_id
);
1168 scoped_ptr
<base::DictionaryValue
> new_entry(new base::DictionaryValue
);
1169 // Set the resource ID and the title
1170 new_entry
->SetString("gd$resourceId.$t", resource_id
);
1171 new_entry
->SetString("title.$t", title
);
1172 new_entry
->SetString("docs$filename", title
);
1173 // Set the content size and MD5 for a file.
1174 if (entry_kind
== "file") {
1175 new_entry
->SetString("docs$size.$t", base::Int64ToString(content_length
));
1176 // TODO(satorux): Set the correct MD5 here.
1177 new_entry
->SetString("docs$md5Checksum.$t",
1178 "3b4385ebefec6e743574c76bbd0575de");
1181 // Add "category" which sets the resource type to |entry_kind|.
1182 base::ListValue
* categories
= new base::ListValue
;
1183 base::DictionaryValue
* category
= new base::DictionaryValue
;
1184 category
->SetString("scheme", "http://schemas.google.com/g/2005#kind");
1185 category
->SetString("term", "http://schemas.google.com/docs/2007#" +
1187 categories
->Append(category
);
1188 new_entry
->Set("category", categories
);
1190 // Add "content" which sets the content URL.
1191 base::DictionaryValue
* content
= new base::DictionaryValue
;
1192 content
->SetString("src", "https://xxx/content/" + resource_id
);
1193 content
->SetString("type", content_type
);
1194 new_entry
->Set("content", content
);
1196 // Add "link" which sets the parent URL, the edit URL and the upload URL.
1197 base::ListValue
* links
= new base::ListValue
;
1199 base::DictionaryValue
* parent_link
= new base::DictionaryValue
;
1200 parent_link
->SetString("href", GetFakeLinkUrl(parent_resource_id
).spec());
1201 parent_link
->SetString("rel",
1202 "http://schemas.google.com/docs/2007#parent");
1203 links
->Append(parent_link
);
1205 base::DictionaryValue
* edit_link
= new base::DictionaryValue
;
1206 edit_link
->SetString("href", "https://xxx/edit/" + resource_id
);
1207 edit_link
->SetString("rel", "edit");
1208 links
->Append(edit_link
);
1210 base::DictionaryValue
* upload_link
= new base::DictionaryValue
;
1211 upload_link
->SetString("href", upload_url
.spec());
1212 upload_link
->SetString("rel", kUploadUrlRel
);
1213 links
->Append(upload_link
);
1214 new_entry
->Set("link", links
);
1216 AddNewChangestamp(new_entry
.get());
1218 // If there are no entries, prepare an empty entry to add.
1219 if (!resource_list_dict
->HasKey("entry"))
1220 resource_list_dict
->Set("entry", new ListValue
);
1222 base::DictionaryValue
* raw_new_entry
= new_entry
.release();
1223 base::ListValue
* entries
= NULL
;
1224 if (resource_list_dict
->GetList("entry", &entries
))
1225 entries
->Append(raw_new_entry
);
1227 return raw_new_entry
;
1230 void FakeDriveService::GetResourceListInternal(
1231 int64 start_changestamp
,
1232 const std::string
& search_query
,
1233 const std::string
& directory_resource_id
,
1236 const GetResourceListCallback
& callback
) {
1238 scoped_ptr
<ResourceList
> null
;
1239 MessageLoop::current()->PostTask(
1241 base::Bind(callback
,
1242 GDATA_NO_CONNECTION
,
1243 base::Passed(&null
)));
1247 scoped_ptr
<ResourceList
> resource_list
=
1248 ResourceList::CreateFrom(*resource_list_value_
);
1250 // Filter out entries per parameters like |directory_resource_id| and
1252 ScopedVector
<ResourceEntry
>* entries
= resource_list
->mutable_entries();
1254 int num_entries_matched
= 0;
1255 for (size_t i
= 0; i
< entries
->size();) {
1256 ResourceEntry
* entry
= (*entries
)[i
];
1257 bool should_exclude
= false;
1259 // If |directory_resource_id| is set, exclude the entry if it's not in
1260 // the target directory.
1261 if (!directory_resource_id
.empty()) {
1262 // Get the parent resource ID of the entry.
1263 std::string parent_resource_id
;
1264 const google_apis::Link
* parent_link
=
1265 entry
->GetLinkByType(Link::LINK_PARENT
);
1267 parent_resource_id
=
1268 net::UnescapeURLComponent(parent_link
->href().ExtractFileName(),
1269 net::UnescapeRule::URL_SPECIAL_CHARS
);
1271 if (directory_resource_id
!= parent_resource_id
)
1272 should_exclude
= true;
1275 // If |search_query| is set, exclude the entry if it does not contain the
1276 // search query in the title.
1277 if (!should_exclude
&& !search_query
.empty() &&
1278 !EntryMatchWithQuery(*entry
, search_query
)) {
1279 should_exclude
= true;
1282 // If |start_changestamp| is set, exclude the entry if the
1283 // changestamp is older than |largest_changestamp|.
1284 // See https://developers.google.com/google-apps/documents-list/
1285 // #retrieving_all_changes_since_a_given_changestamp
1286 if (start_changestamp
> 0 && entry
->changestamp() < start_changestamp
)
1287 should_exclude
= true;
1289 // The entry matched the criteria for inclusion.
1290 if (!should_exclude
)
1291 ++num_entries_matched
;
1293 // If |start_offset| is set, exclude the entry if the entry is before the
1294 // start index. <= instead of < as |num_entries_matched| was
1295 // already incremented.
1296 if (start_offset
> 0 && num_entries_matched
<= start_offset
)
1297 should_exclude
= true;
1300 entries
->erase(entries
->begin() + i
);
1305 // If |max_results| is set, trim the entries if the number exceeded the max
1307 if (max_results
> 0 && entries
->size() > static_cast<size_t>(max_results
)) {
1308 entries
->erase(entries
->begin() + max_results
, entries
->end());
1309 // Adds the next URL.
1310 // Here, we embed information which is needed for continueing the
1311 // GetResourceList operation in the next invocation into url query
1313 GURL
next_url(base::StringPrintf(
1314 "http://localhost/?start-offset=%d&max-results=%d",
1315 start_offset
+ max_results
,
1317 if (start_changestamp
> 0) {
1318 next_url
= net::AppendOrReplaceQueryParameter(
1319 next_url
, "changestamp",
1320 base::Int64ToString(start_changestamp
).c_str());
1322 if (!search_query
.empty()) {
1323 next_url
= net::AppendOrReplaceQueryParameter(
1324 next_url
, "q", search_query
);
1326 if (!directory_resource_id
.empty()) {
1327 next_url
= net::AppendOrReplaceQueryParameter(
1328 next_url
, "parent", directory_resource_id
);
1331 Link
* link
= new Link
;
1332 link
->set_type(Link::LINK_NEXT
);
1333 link
->set_href(next_url
);
1334 resource_list
->mutable_links()->push_back(link
);
1337 ++resource_list_load_count_
;
1338 MessageLoop::current()->PostTask(
1340 base::Bind(callback
,
1342 base::Passed(&resource_list
)));
1345 } // namespace google_apis