// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/drive/fake_drive_service.h"
#include <string>
#include "base/file_util.h"
#include "base/logging.h"
#include "base/md5.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/drive/drive_api_util.h"
#include "content/public/browser/browser_thread.h"
#include "google_apis/drive/drive_api_parser.h"
#include "google_apis/drive/gdata_wapi_parser.h"
#include "google_apis/drive/test_util.h"
#include "google_apis/drive/time_util.h"
#include "net/base/escape.h"
#include "net/base/url_util.h"
using content::BrowserThread;
using google_apis::AboutResource;
using google_apis::AboutResourceCallback;
using google_apis::AccountMetadata;
using google_apis::AppList;
using google_apis::AppListCallback;
using google_apis::AuthStatusCallback;
using google_apis::AuthorizeAppCallback;
using google_apis::CancelCallback;
using google_apis::DownloadActionCallback;
using google_apis::EntryActionCallback;
using google_apis::GDATA_FILE_ERROR;
using google_apis::GDATA_NO_CONNECTION;
using google_apis::GDATA_OTHER_ERROR;
using google_apis::GDataErrorCode;
using google_apis::GetContentCallback;
using google_apis::GetResourceEntryCallback;
using google_apis::GetResourceListCallback;
using google_apis::GetShareUrlCallback;
using google_apis::HTTP_BAD_REQUEST;
using google_apis::HTTP_CREATED;
using google_apis::HTTP_NOT_FOUND;
using google_apis::HTTP_NO_CONTENT;
using google_apis::HTTP_PRECONDITION;
using google_apis::HTTP_RESUME_INCOMPLETE;
using google_apis::HTTP_SUCCESS;
using google_apis::InitiateUploadCallback;
using google_apis::Link;
using google_apis::ProgressCallback;
using google_apis::ResourceEntry;
using google_apis::ResourceList;
using google_apis::UploadRangeCallback;
using google_apis::UploadRangeResponse;
namespace test_util = google_apis::test_util;
namespace drive {
namespace {
// Rel property of an upload link in the entries dictionary value.
const char kUploadUrlRel[] =
"http://schemas.google.com/g/2005#resumable-create-media";
// Rel property of a share link in the entries dictionary value.
const char kShareUrlRel[] =
"http://schemas.google.com/docs/2007#share";
// Returns true if a resource entry matches with the search query.
// Supports queries consist of following format.
// - Phrases quoted by double/single quotes
// - AND search for multiple words/phrases segmented by space
// - Limited attribute search. Only "title:" is supported.
bool EntryMatchWithQuery(const ResourceEntry& entry,
const std::string& query) {
base::StringTokenizer tokenizer(query, " ");
tokenizer.set_quote_chars("\"'");
while (tokenizer.GetNext()) {
std::string key, value;
const std::string& token = tokenizer.token();
if (token.find(':') == std::string::npos) {
base::TrimString(token, "\"'", &value);
} else {
base::StringTokenizer key_value(token, ":");
key_value.set_quote_chars("\"'");
if (!key_value.GetNext())
return false;
key = key_value.token();
if (!key_value.GetNext())
return false;
base::TrimString(key_value.token(), "\"'", &value);
}
// TODO(peria): Deal with other attributes than title.
if (!key.empty() && key != "title")
return false;
// Search query in the title.
if (entry.title().find(value) == std::string::npos)
return false;
}
return true;
}
void ScheduleUploadRangeCallback(const UploadRangeCallback& callback,
int64 start_position,
int64 end_position,
GDataErrorCode error,
scoped_ptr<ResourceEntry> entry) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
UploadRangeResponse(error,
start_position,
end_position),
base::Passed(&entry)));
}
void EntryActionCallbackAdapter(
const EntryActionCallback& callback,
GDataErrorCode error, scoped_ptr<ResourceEntry> resource_entry) {
callback.Run(error);
}
void ClearPropatiesForPermanentDelete(DictionaryValue* entry) {
scoped_ptr<DictionaryValue> new_entry(new DictionaryValue);
const char* kPreservedProperties[] = {
"docs$removed", "docs$changestamp", "gd$resourceId", "id", "updated"
};
for (size_t i = 0; i < arraysize(kPreservedProperties); ++i) {
const char* key = kPreservedProperties[i];
scoped_ptr<Value> value;
if (entry->Remove(key, &value))
new_entry->Set(key, value.release());
}
entry->Swap(new_entry.get());
}
} // namespace
struct FakeDriveService::UploadSession {
std::string content_type;
int64 content_length;
std::string parent_resource_id;
std::string resource_id;
std::string etag;
std::string title;
int64 uploaded_size;
UploadSession()
: content_length(0),
uploaded_size(0) {}
UploadSession(
std::string content_type,
int64 content_length,
std::string parent_resource_id,
std::string resource_id,
std::string etag,
std::string title)
: content_type(content_type),
content_length(content_length),
parent_resource_id(parent_resource_id),
resource_id(resource_id),
etag(etag),
title(title),
uploaded_size(0) {
}
};
FakeDriveService::FakeDriveService()
: largest_changestamp_(0),
published_date_seq_(0),
next_upload_sequence_number_(0),
default_max_results_(0),
resource_id_count_(0),
resource_list_load_count_(0),
change_list_load_count_(0),
directory_load_count_(0),
about_resource_load_count_(0),
app_list_load_count_(0),
blocked_resource_list_load_count_(0),
offline_(false),
never_return_all_resource_list_(false) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
FakeDriveService::~FakeDriveService() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
bool FakeDriveService::LoadResourceListForWapi(
const std::string& relative_path) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
scoped_ptr<Value> raw_value = test_util::LoadJSONFile(relative_path);
base::DictionaryValue* as_dict = NULL;
scoped_ptr<base::Value> feed;
base::DictionaryValue* feed_as_dict = NULL;
// Extract the "feed" from the raw value and take the ownership.
// Note that Remove() transfers the ownership to |feed|.
if (raw_value->GetAsDictionary(&as_dict) &&
as_dict->Remove("feed", &feed) &&
feed->GetAsDictionary(&feed_as_dict)) {
ignore_result(feed.release());
resource_list_value_.reset(feed_as_dict);
// Go through entries and convert test$data from a string to a binary blob.
base::ListValue* entries = NULL;
if (feed_as_dict->GetList("entry", &entries)) {
for (size_t i = 0; i < entries->GetSize(); ++i) {
base::DictionaryValue* entry = NULL;
std::string content_data;
if (entries->GetDictionary(i, &entry) &&
entry->GetString("test$data", &content_data)) {
entry->Set("test$data",
base::BinaryValue::CreateWithCopiedBuffer(
content_data.c_str(), content_data.size()));
}
}
}
}
return resource_list_value_;
}
bool FakeDriveService::LoadAccountMetadataForWapi(
const std::string& relative_path) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// Load JSON data, which must be a dictionary.
scoped_ptr<base::Value> value = test_util::LoadJSONFile(relative_path);
CHECK_EQ(base::Value::TYPE_DICTIONARY, value->GetType());
account_metadata_value_.reset(
static_cast<base::DictionaryValue*>(value.release()));
// Update the largest_changestamp_.
scoped_ptr<AccountMetadata> account_metadata =
AccountMetadata::CreateFrom(*account_metadata_value_);
largest_changestamp_ = account_metadata->largest_changestamp();
// Add the largest changestamp to the existing entries.
// This will be used to generate change lists in GetResourceList().
if (resource_list_value_) {
base::ListValue* entries = NULL;
if (resource_list_value_->GetList("entry", &entries)) {
for (size_t i = 0; i < entries->GetSize(); ++i) {
base::DictionaryValue* entry = NULL;
if (entries->GetDictionary(i, &entry)) {
entry->SetString("docs$changestamp.value",
base::Int64ToString(largest_changestamp_));
}
}
}
}
return account_metadata_value_;
}
bool FakeDriveService::LoadAppListForDriveApi(
const std::string& relative_path) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
app_info_value_ = test_util::LoadJSONFile(relative_path);
return app_info_value_;
}
void FakeDriveService::SetQuotaValue(int64 used, int64 total) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(account_metadata_value_);
account_metadata_value_->SetString("entry.gd$quotaBytesUsed.$t",
base::Int64ToString16(used));
account_metadata_value_->SetString("entry.gd$quotaBytesTotal.$t",
base::Int64ToString16(total));
}
GURL FakeDriveService::GetFakeLinkUrl(const std::string& resource_id) {
return GURL("https://fake_server/" + net::EscapePath(resource_id));
}
void FakeDriveService::Initialize(const std::string& account_id) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
void FakeDriveService::AddObserver(DriveServiceObserver* observer) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
void FakeDriveService::RemoveObserver(DriveServiceObserver* observer) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
bool FakeDriveService::CanSendRequest() const {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
return true;
}
ResourceIdCanonicalizer FakeDriveService::GetResourceIdCanonicalizer() const {
return util::GetIdentityResourceIdCanonicalizer();
}
bool FakeDriveService::HasAccessToken() const {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
return true;
}
void FakeDriveService::RequestAccessToken(const AuthStatusCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
callback.Run(google_apis::HTTP_NOT_MODIFIED, "fake_access_token");
}
bool FakeDriveService::HasRefreshToken() const {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
return true;
}
void FakeDriveService::ClearAccessToken() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
void FakeDriveService::ClearRefreshToken() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
}
std::string FakeDriveService::GetRootResourceId() const {
return "fake_root";
}
CancelCallback FakeDriveService::GetAllResourceList(
const GetResourceListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (never_return_all_resource_list_) {
++blocked_resource_list_load_count_;
return CancelCallback();
}
GetResourceListInternal(0, // start changestamp
std::string(), // empty search query
std::string(), // no directory resource id,
0, // start offset
default_max_results_,
&resource_list_load_count_,
callback);
return CancelCallback();
}
CancelCallback FakeDriveService::GetResourceListInDirectory(
const std::string& directory_resource_id,
const GetResourceListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!directory_resource_id.empty());
DCHECK(!callback.is_null());
GetResourceListInternal(0, // start changestamp
std::string(), // empty search query
directory_resource_id,
0, // start offset
default_max_results_,
&directory_load_count_,
callback);
return CancelCallback();
}
CancelCallback FakeDriveService::Search(
const std::string& search_query,
const GetResourceListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!search_query.empty());
DCHECK(!callback.is_null());
GetResourceListInternal(0, // start changestamp
search_query,
std::string(), // no directory resource id,
0, // start offset
default_max_results_,
NULL,
callback);
return CancelCallback();
}
CancelCallback FakeDriveService::SearchByTitle(
const std::string& title,
const std::string& directory_resource_id,
const GetResourceListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!title.empty());
DCHECK(!callback.is_null());
// Note: the search implementation here doesn't support quotation unescape,
// so don't escape here.
GetResourceListInternal(0, // start changestamp
base::StringPrintf("title:'%s'", title.c_str()),
directory_resource_id,
0, // start offset
default_max_results_,
NULL,
callback);
return CancelCallback();
}
CancelCallback FakeDriveService::GetChangeList(
int64 start_changestamp,
const GetResourceListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
GetResourceListInternal(start_changestamp,
std::string(), // empty search query
std::string(), // no directory resource id,
0, // start offset
default_max_results_,
&change_list_load_count_,
callback);
return CancelCallback();
}
CancelCallback FakeDriveService::GetRemainingChangeList(
const GURL& next_link,
const GetResourceListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!next_link.is_empty());
DCHECK(!callback.is_null());
return GetRemainingResourceList(next_link, callback);
}
CancelCallback FakeDriveService::GetRemainingFileList(
const GURL& next_link,
const GetResourceListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!next_link.is_empty());
DCHECK(!callback.is_null());
return GetRemainingResourceList(next_link, callback);
}
CancelCallback FakeDriveService::GetResourceEntry(
const std::string& resource_id,
const GetResourceEntryCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION,
base::Passed(&null)));
return CancelCallback();
}
base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
if (entry) {
scoped_ptr<ResourceEntry> resource_entry =
ResourceEntry::CreateFrom(*entry);
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry)));
return CancelCallback();
}
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
return CancelCallback();
}
CancelCallback FakeDriveService::GetShareUrl(
const std::string& resource_id,
const GURL& /* embed_origin */,
const GetShareUrlCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION,
GURL()));
return CancelCallback();
}
base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
if (entry) {
// Share urls are stored in the resource entry, and they do not rely on the
// embedding origin.
scoped_ptr<ResourceEntry> resource_entry =
ResourceEntry::CreateFrom(*entry);
const Link* share_url = resource_entry->GetLinkByType(Link::LINK_SHARE);
if (share_url) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, share_url->href()));
} else {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, GURL()));
}
return CancelCallback();
}
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND, GURL()));
return CancelCallback();
}
CancelCallback FakeDriveService::GetAboutResource(
const AboutResourceCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
scoped_ptr<AboutResource> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION, base::Passed(&null)));
return CancelCallback();
}
++about_resource_load_count_;
scoped_ptr<AboutResource> about_resource(
util::ConvertAccountMetadataToAboutResource(
*AccountMetadata::CreateFrom(*account_metadata_value_),
GetRootResourceId()));
// Overwrite the change id.
about_resource->set_largest_change_id(largest_changestamp_);
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
HTTP_SUCCESS, base::Passed(&about_resource)));
return CancelCallback();
}
CancelCallback FakeDriveService::GetAppList(const AppListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
DCHECK(app_info_value_);
if (offline_) {
scoped_ptr<AppList> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION,
base::Passed(&null)));
return CancelCallback();
}
++app_list_load_count_;
scoped_ptr<AppList> app_list(AppList::CreateFrom(*app_info_value_));
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, base::Passed(&app_list)));
return CancelCallback();
}
CancelCallback FakeDriveService::DeleteResource(
const std::string& resource_id,
const std::string& etag,
const EntryActionCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
return CancelCallback();
}
base::ListValue* entries = NULL;
if (resource_list_value_->GetList("entry", &entries)) {
for (size_t i = 0; i < entries->GetSize(); ++i) {
base::DictionaryValue* entry = NULL;
std::string current_resource_id;
if (entries->GetDictionary(i, &entry) &&
entry->GetString("gd$resourceId.$t", ¤t_resource_id) &&
resource_id == current_resource_id) {
if (entry->HasKey("docs$removed")) {
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
return CancelCallback();
}
std::string entry_etag;
entry->GetString("gd$etag", &entry_etag);
if (!etag.empty() && entry_etag != etag) {
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_PRECONDITION));
return CancelCallback();
}
entry->Set("docs$removed", new DictionaryValue);
AddNewChangestamp(entry);
ClearPropatiesForPermanentDelete(entry);
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_NO_CONTENT));
return CancelCallback();
}
}
}
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
return CancelCallback();
}
CancelCallback FakeDriveService::TrashResource(
const std::string& resource_id,
const EntryActionCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
return CancelCallback();
}
base::ListValue* entries = NULL;
// Go through entries and remove the one that matches |resource_id|.
if (resource_list_value_->GetList("entry", &entries)) {
for (size_t i = 0; i < entries->GetSize(); ++i) {
base::DictionaryValue* entry = NULL;
std::string current_resource_id;
if (entries->GetDictionary(i, &entry) &&
entry->GetString("gd$resourceId.$t", ¤t_resource_id) &&
resource_id == current_resource_id) {
GDataErrorCode error = google_apis::GDATA_OTHER_ERROR;
if (entry->HasKey("gd$deleted") || entry->HasKey("docs$removed")) {
error = HTTP_NOT_FOUND;
} else {
entry->Set("gd$deleted", new DictionaryValue);
AddNewChangestamp(entry);
error = HTTP_SUCCESS;
}
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, error));
return CancelCallback();
}
}
}
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
return CancelCallback();
}
CancelCallback FakeDriveService::DownloadFile(
const base::FilePath& local_cache_path,
const std::string& resource_id,
const DownloadActionCallback& download_action_callback,
const GetContentCallback& get_content_callback,
const ProgressCallback& progress_callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!download_action_callback.is_null());
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(download_action_callback,
GDATA_NO_CONNECTION,
base::FilePath()));
return CancelCallback();
}
// The field content.src is the URL to download the file.
base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
if (!entry) {
base::MessageLoopProxy::current()->PostTask(
FROM_HERE,
base::Bind(download_action_callback, HTTP_NOT_FOUND, base::FilePath()));
return CancelCallback();
}
// Write "x"s of the file size specified in the entry.
std::string file_size_string;
entry->GetString("docs$size.$t", &file_size_string);
int64 file_size = 0;
if (base::StringToInt64(file_size_string, &file_size)) {
base::BinaryValue* content_binary_data;
std::string content_data;
if (entry->GetBinary("test$data", &content_binary_data)) {
content_data = std::string(content_binary_data->GetBuffer(),
content_binary_data->GetSize());
}
DCHECK_EQ(static_cast<size_t>(file_size), content_data.size());
if (!get_content_callback.is_null()) {
const int64 kBlockSize = 5;
for (int64 i = 0; i < file_size; i += kBlockSize) {
const int64 size = std::min(kBlockSize, file_size - i);
scoped_ptr<std::string> content_for_callback(
new std::string(content_data.substr(i, size)));
base::MessageLoopProxy::current()->PostTask(
FROM_HERE,
base::Bind(get_content_callback, HTTP_SUCCESS,
base::Passed(&content_for_callback)));
}
}
if (test_util::WriteStringToFile(local_cache_path, content_data)) {
if (!progress_callback.is_null()) {
// See also the comment in ResumeUpload(). For testing that clients
// can handle the case progress_callback is called multiple times,
// here we invoke the callback twice.
base::MessageLoopProxy::current()->PostTask(
FROM_HERE,
base::Bind(progress_callback, file_size / 2, file_size));
base::MessageLoopProxy::current()->PostTask(
FROM_HERE,
base::Bind(progress_callback, file_size, file_size));
}
base::MessageLoopProxy::current()->PostTask(
FROM_HERE,
base::Bind(download_action_callback,
HTTP_SUCCESS,
local_cache_path));
return CancelCallback();
}
}
// Failed to write the content.
base::MessageLoopProxy::current()->PostTask(
FROM_HERE,
base::Bind(download_action_callback, GDATA_FILE_ERROR, base::FilePath()));
return CancelCallback();
}
CancelCallback FakeDriveService::CopyResource(
const std::string& resource_id,
const std::string& in_parent_resource_id,
const std::string& new_title,
const base::Time& last_modified,
const GetResourceEntryCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION,
base::Passed(&null)));
return CancelCallback();
}
const std::string& parent_resource_id = in_parent_resource_id.empty() ?
GetRootResourceId() : in_parent_resource_id;
base::ListValue* entries = NULL;
// Go through entries and copy the one that matches |resource_id|.
if (resource_list_value_->GetList("entry", &entries)) {
for (size_t i = 0; i < entries->GetSize(); ++i) {
base::DictionaryValue* entry = NULL;
std::string current_resource_id;
if (entries->GetDictionary(i, &entry) &&
entry->GetString("gd$resourceId.$t", ¤t_resource_id) &&
resource_id == current_resource_id) {
// Make a copy and set the new resource ID and the new title.
scoped_ptr<DictionaryValue> copied_entry(entry->DeepCopy());
copied_entry->SetString("gd$resourceId.$t",
resource_id + "_copied");
copied_entry->SetString("title.$t", new_title);
// Reset parent directory.
base::ListValue* links = NULL;
if (!copied_entry->GetList("link", &links)) {
links = new base::ListValue;
copied_entry->Set("link", links);
}
links->Clear();
base::DictionaryValue* link = new base::DictionaryValue;
link->SetString(
"rel", "http://schemas.google.com/docs/2007#parent");
link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec());
links->Append(link);
if (!last_modified.is_null()) {
copied_entry->SetString(
"updated.$t",
google_apis::util::FormatTimeAsString(last_modified));
}
AddNewChangestamp(copied_entry.get());
UpdateETag(copied_entry.get());
// Parse the new entry.
scoped_ptr<ResourceEntry> resource_entry =
ResourceEntry::CreateFrom(*copied_entry);
// Add it to the resource list.
entries->Append(copied_entry.release());
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
HTTP_SUCCESS,
base::Passed(&resource_entry)));
return CancelCallback();
}
}
}
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
return CancelCallback();
}
CancelCallback FakeDriveService::UpdateResource(
const std::string& resource_id,
const std::string& parent_resource_id,
const std::string& new_title,
const base::Time& last_modified,
const base::Time& last_viewed_by_me,
const google_apis::GetResourceEntryCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION,
base::Passed(scoped_ptr<ResourceEntry>())));
return CancelCallback();
}
base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
if (entry) {
entry->SetString("title.$t", new_title);
// Set parent if necessary.
if (!parent_resource_id.empty()) {
base::ListValue* links = NULL;
if (!entry->GetList("link", &links)) {
links = new base::ListValue;
entry->Set("link", links);
}
// Remove old parent(s).
for (size_t i = 0; i < links->GetSize(); ) {
base::DictionaryValue* link = NULL;
std::string rel;
std::string href;
if (links->GetDictionary(i, &link) &&
link->GetString("rel", &rel) &&
link->GetString("href", &href) &&
rel == "http://schemas.google.com/docs/2007#parent") {
links->Remove(i, NULL);
} else {
++i;
}
}
base::DictionaryValue* link = new base::DictionaryValue;
link->SetString("rel", "http://schemas.google.com/docs/2007#parent");
link->SetString(
"href", GetFakeLinkUrl(parent_resource_id).spec());
links->Append(link);
}
if (!last_modified.is_null()) {
entry->SetString(
"updated.$t",
google_apis::util::FormatTimeAsString(last_modified));
}
if (!last_viewed_by_me.is_null()) {
entry->SetString(
"gd$lastViewed.$t",
google_apis::util::FormatTimeAsString(last_viewed_by_me));
}
AddNewChangestamp(entry);
UpdateETag(entry);
// Parse the new entry.
scoped_ptr<ResourceEntry> resource_entry =
ResourceEntry::CreateFrom(*entry);
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_entry)));
return CancelCallback();
}
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND,
base::Passed(scoped_ptr<ResourceEntry>())));
return CancelCallback();
}
CancelCallback FakeDriveService::RenameResource(
const std::string& resource_id,
const std::string& new_title,
const EntryActionCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
return UpdateResource(
resource_id, std::string(), new_title, base::Time(), base::Time(),
base::Bind(&EntryActionCallbackAdapter, callback));
}
CancelCallback FakeDriveService::AddResourceToDirectory(
const std::string& parent_resource_id,
const std::string& resource_id,
const EntryActionCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
return CancelCallback();
}
base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
if (entry) {
base::ListValue* links = NULL;
if (!entry->GetList("link", &links)) {
links = new base::ListValue;
entry->Set("link", links);
}
// On the real Drive server, resources do not necessary shape a tree
// structure. That is, each resource can have multiple parent.
// We mimic the behavior here; AddResourceToDirectoy just adds
// one more parent link, not overwriting old links.
base::DictionaryValue* link = new base::DictionaryValue;
link->SetString("rel", "http://schemas.google.com/docs/2007#parent");
link->SetString(
"href", GetFakeLinkUrl(parent_resource_id).spec());
links->Append(link);
AddNewChangestamp(entry);
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_SUCCESS));
return CancelCallback();
}
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
return CancelCallback();
}
CancelCallback FakeDriveService::RemoveResourceFromDirectory(
const std::string& parent_resource_id,
const std::string& resource_id,
const EntryActionCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, GDATA_NO_CONNECTION));
return CancelCallback();
}
base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
if (entry) {
base::ListValue* links = NULL;
if (entry->GetList("link", &links)) {
GURL parent_content_url = GetFakeLinkUrl(parent_resource_id);
for (size_t i = 0; i < links->GetSize(); ++i) {
base::DictionaryValue* link = NULL;
std::string rel;
std::string href;
if (links->GetDictionary(i, &link) &&
link->GetString("rel", &rel) &&
link->GetString("href", &href) &&
rel == "http://schemas.google.com/docs/2007#parent" &&
GURL(href) == parent_content_url) {
links->Remove(i, NULL);
AddNewChangestamp(entry);
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_NO_CONTENT));
return CancelCallback();
}
}
}
}
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(callback, HTTP_NOT_FOUND));
return CancelCallback();
}
CancelCallback FakeDriveService::AddNewDirectory(
const std::string& parent_resource_id,
const std::string& directory_title,
const GetResourceEntryCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION,
base::Passed(&null)));
return CancelCallback();
}
const char kContentType[] = "application/atom+xml;type=feed";
const base::DictionaryValue* new_entry = AddNewEntry(kContentType,
"", // content_data
parent_resource_id,
directory_title,
false, // shared_with_me
"folder");
if (!new_entry) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
return CancelCallback();
}
scoped_ptr<ResourceEntry> parsed_entry(ResourceEntry::CreateFrom(*new_entry));
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry)));
return CancelCallback();
}
CancelCallback FakeDriveService::InitiateUploadNewFile(
const std::string& content_type,
int64 content_length,
const std::string& parent_resource_id,
const std::string& title,
const InitiateUploadCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, GDATA_NO_CONNECTION, GURL()));
return CancelCallback();
}
if (parent_resource_id != GetRootResourceId() &&
!FindEntryByResourceId(parent_resource_id)) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND, GURL()));
return CancelCallback();
}
GURL session_url = GetNewUploadSessionUrl();
upload_sessions_[session_url] =
UploadSession(content_type, content_length,
parent_resource_id,
"", // resource_id
"", // etag
title);
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, session_url));
return CancelCallback();
}
CancelCallback FakeDriveService::InitiateUploadExistingFile(
const std::string& content_type,
int64 content_length,
const std::string& resource_id,
const std::string& etag,
const InitiateUploadCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, GDATA_NO_CONNECTION, GURL()));
return CancelCallback();
}
DictionaryValue* entry = FindEntryByResourceId(resource_id);
if (!entry) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND, GURL()));
return CancelCallback();
}
std::string entry_etag;
entry->GetString("gd$etag", &entry_etag);
if (!etag.empty() && etag != entry_etag) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_PRECONDITION, GURL()));
return CancelCallback();
}
GURL session_url = GetNewUploadSessionUrl();
upload_sessions_[session_url] =
UploadSession(content_type, content_length,
"", // parent_resource_id
resource_id,
entry_etag,
"" /* title */);
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, session_url));
return CancelCallback();
}
CancelCallback FakeDriveService::GetUploadStatus(
const GURL& upload_url,
int64 content_length,
const UploadRangeCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
return CancelCallback();
}
CancelCallback FakeDriveService::ResumeUpload(
const GURL& upload_url,
int64 start_position,
int64 end_position,
int64 content_length,
const std::string& content_type,
const base::FilePath& local_file_path,
const UploadRangeCallback& callback,
const ProgressCallback& progress_callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
GetResourceEntryCallback completion_callback
= base::Bind(&ScheduleUploadRangeCallback,
callback, start_position, end_position);
if (offline_) {
completion_callback.Run(GDATA_NO_CONNECTION, scoped_ptr<ResourceEntry>());
return CancelCallback();
}
if (!upload_sessions_.count(upload_url)) {
completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
return CancelCallback();
}
UploadSession* session = &upload_sessions_[upload_url];
// Chunks are required to be sent in such a ways that they fill from the start
// of the not-yet-uploaded part with no gaps nor overlaps.
if (session->uploaded_size != start_position) {
completion_callback.Run(HTTP_BAD_REQUEST, scoped_ptr<ResourceEntry>());
return CancelCallback();
}
if (!progress_callback.is_null()) {
// In the real GDataWapi/Drive DriveService, progress is reported in
// nondeterministic timing. In this fake implementation, we choose to call
// it twice per one ResumeUpload. This is for making sure that client code
// works fine even if the callback is invoked more than once; it is the
// crucial difference of the progress callback from others.
// Note that progress is notified in the relative offset in each chunk.
const int64 chunk_size = end_position - start_position;
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(progress_callback, chunk_size / 2, chunk_size));
base::MessageLoop::current()->PostTask(
FROM_HERE, base::Bind(progress_callback, chunk_size, chunk_size));
}
if (content_length != end_position) {
session->uploaded_size = end_position;
completion_callback.Run(HTTP_RESUME_INCOMPLETE,
scoped_ptr<ResourceEntry>());
return CancelCallback();
}
std::string content_data;
if (!base::ReadFileToString(local_file_path, &content_data)) {
session->uploaded_size = end_position;
completion_callback.Run(GDATA_FILE_ERROR, scoped_ptr<ResourceEntry>());
return CancelCallback();
}
session->uploaded_size = end_position;
// |resource_id| is empty if the upload is for new file.
if (session->resource_id.empty()) {
DCHECK(!session->parent_resource_id.empty());
DCHECK(!session->title.empty());
const DictionaryValue* new_entry = AddNewEntry(
session->content_type,
content_data,
session->parent_resource_id,
session->title,
false, // shared_with_me
"file");
if (!new_entry) {
completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
return CancelCallback();
}
completion_callback.Run(HTTP_CREATED,
ResourceEntry::CreateFrom(*new_entry));
return CancelCallback();
}
DictionaryValue* entry = FindEntryByResourceId(session->resource_id);
if (!entry) {
completion_callback.Run(HTTP_NOT_FOUND, scoped_ptr<ResourceEntry>());
return CancelCallback();
}
std::string entry_etag;
entry->GetString("gd$etag", &entry_etag);
if (entry_etag.empty() || session->etag != entry_etag) {
completion_callback.Run(HTTP_PRECONDITION, scoped_ptr<ResourceEntry>());
return CancelCallback();
}
entry->SetString("docs$md5Checksum.$t", base::MD5String(content_data));
entry->Set("test$data",
base::BinaryValue::CreateWithCopiedBuffer(
content_data.data(), content_data.size()));
entry->SetString("docs$size.$t", base::Int64ToString(end_position));
AddNewChangestamp(entry);
UpdateETag(entry);
completion_callback.Run(HTTP_SUCCESS, ResourceEntry::CreateFrom(*entry));
return CancelCallback();
}
CancelCallback FakeDriveService::AuthorizeApp(
const std::string& resource_id,
const std::string& app_id,
const AuthorizeAppCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
return CancelCallback();
}
CancelCallback FakeDriveService::GetResourceListInDirectoryByWapi(
const std::string& directory_resource_id,
const google_apis::GetResourceListCallback& callback) {
return GetResourceListInDirectory(
directory_resource_id == util::kWapiRootDirectoryResourceId ?
GetRootResourceId() :
directory_resource_id,
callback);
}
CancelCallback FakeDriveService::GetRemainingResourceList(
const GURL& next_link,
const google_apis::GetResourceListCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!next_link.is_empty());
DCHECK(!callback.is_null());
// "changestamp", "q", "parent" and "start-offset" are parameters to
// implement "paging" of the result on FakeDriveService.
// The URL should be the one filled in GetResourceListInternal of the
// previous method invocation, so it should start with "http://localhost/?".
// See also GetResourceListInternal.
DCHECK_EQ(next_link.host(), "localhost");
DCHECK_EQ(next_link.path(), "/");
int64 start_changestamp = 0;
std::string search_query;
std::string directory_resource_id;
int start_offset = 0;
int max_results = default_max_results_;
std::vector<std::pair<std::string, std::string> > parameters;
if (base::SplitStringIntoKeyValuePairs(
next_link.query(), '=', '&', ¶meters)) {
for (size_t i = 0; i < parameters.size(); ++i) {
if (parameters[i].first == "changestamp") {
base::StringToInt64(parameters[i].second, &start_changestamp);
} else if (parameters[i].first == "q") {
search_query =
net::UnescapeURLComponent(parameters[i].second,
net::UnescapeRule::URL_SPECIAL_CHARS);
} else if (parameters[i].first == "parent") {
directory_resource_id =
net::UnescapeURLComponent(parameters[i].second,
net::UnescapeRule::URL_SPECIAL_CHARS);
} else if (parameters[i].first == "start-offset") {
base::StringToInt(parameters[i].second, &start_offset);
} else if (parameters[i].first == "max-results") {
base::StringToInt(parameters[i].second, &max_results);
}
}
}
GetResourceListInternal(
start_changestamp, search_query, directory_resource_id,
start_offset, max_results, NULL, callback);
return CancelCallback();
}
void FakeDriveService::AddNewFile(const std::string& content_type,
const std::string& content_data,
const std::string& parent_resource_id,
const std::string& title,
bool shared_with_me,
const GetResourceEntryCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION,
base::Passed(&null)));
return;
}
// Prepare "kind" for hosted documents. This only supports Google Document.
std::string entry_kind;
if (content_type == "application/vnd.google-apps.document")
entry_kind = "document";
else
entry_kind = "file";
const base::DictionaryValue* new_entry = AddNewEntry(content_type,
content_data,
parent_resource_id,
title,
shared_with_me,
entry_kind);
if (!new_entry) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
return;
}
scoped_ptr<ResourceEntry> parsed_entry(
ResourceEntry::CreateFrom(*new_entry));
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_CREATED, base::Passed(&parsed_entry)));
}
void FakeDriveService::SetLastModifiedTime(
const std::string& resource_id,
const base::Time& last_modified_time,
const GetResourceEntryCallback& callback) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DCHECK(!callback.is_null());
if (offline_) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION,
base::Passed(&null)));
return;
}
base::DictionaryValue* entry = FindEntryByResourceId(resource_id);
if (!entry) {
scoped_ptr<ResourceEntry> null;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_NOT_FOUND, base::Passed(&null)));
return;
}
if (last_modified_time.is_null()) {
entry->Remove("updated.$t", NULL);
} else {
entry->SetString(
"updated.$t",
google_apis::util::FormatTimeAsString(last_modified_time));
}
scoped_ptr<ResourceEntry> parsed_entry(
ResourceEntry::CreateFrom(*entry));
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, base::Passed(&parsed_entry)));
}
base::DictionaryValue* FakeDriveService::FindEntryByResourceId(
const std::string& resource_id) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
base::ListValue* entries = NULL;
// Go through entries and return the one that matches |resource_id|.
if (resource_list_value_->GetList("entry", &entries)) {
for (size_t i = 0; i < entries->GetSize(); ++i) {
base::DictionaryValue* entry = NULL;
std::string current_resource_id;
if (entries->GetDictionary(i, &entry) &&
entry->GetString("gd$resourceId.$t", ¤t_resource_id) &&
resource_id == current_resource_id) {
return entry;
}
}
}
return NULL;
}
base::DictionaryValue* FakeDriveService::FindEntryByContentUrl(
const GURL& content_url) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
base::ListValue* entries = NULL;
// Go through entries and return the one that matches |content_url|.
if (resource_list_value_->GetList("entry", &entries)) {
for (size_t i = 0; i < entries->GetSize(); ++i) {
base::DictionaryValue* entry = NULL;
std::string current_content_url;
if (entries->GetDictionary(i, &entry) &&
entry->GetString("content.src", ¤t_content_url) &&
content_url == GURL(current_content_url)) {
return entry;
}
}
}
return NULL;
}
std::string FakeDriveService::GetNewResourceId() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
++resource_id_count_;
return base::StringPrintf("resource_id_%d", resource_id_count_);
}
void FakeDriveService::UpdateETag(base::DictionaryValue* entry) {
entry->SetString("gd$etag",
"etag_" + base::Int64ToString(largest_changestamp_));
}
void FakeDriveService::AddNewChangestamp(base::DictionaryValue* entry) {
++largest_changestamp_;
entry->SetString("docs$changestamp.value",
base::Int64ToString(largest_changestamp_));
}
const base::DictionaryValue* FakeDriveService::AddNewEntry(
const std::string& content_type,
const std::string& content_data,
const std::string& parent_resource_id,
const std::string& title,
bool shared_with_me,
const std::string& entry_kind) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (!parent_resource_id.empty() &&
parent_resource_id != GetRootResourceId() &&
!FindEntryByResourceId(parent_resource_id)) {
return NULL;
}
std::string resource_id = GetNewResourceId();
GURL upload_url = GURL("https://xxx/upload/" + resource_id);
scoped_ptr<base::DictionaryValue> new_entry(new base::DictionaryValue);
// Set the resource ID and the title
new_entry->SetString("gd$resourceId.$t", resource_id);
new_entry->SetString("title.$t", title);
new_entry->SetString("docs$filename", title);
// Set the contents, size and MD5 for a file.
if (entry_kind == "file") {
new_entry->Set("test$data",
base::BinaryValue::CreateWithCopiedBuffer(
content_data.c_str(), content_data.size()));
new_entry->SetString("docs$size.$t",
base::Int64ToString(content_data.size()));
new_entry->SetString("docs$md5Checksum.$t",
base::MD5String(content_data));
}
// Add "category" which sets the resource type to |entry_kind|.
base::ListValue* categories = new base::ListValue;
base::DictionaryValue* category = new base::DictionaryValue;
category->SetString("scheme", "http://schemas.google.com/g/2005#kind");
category->SetString("term", "http://schemas.google.com/docs/2007#" +
entry_kind);
categories->Append(category);
new_entry->Set("category", categories);
if (shared_with_me) {
base::DictionaryValue* shared_with_me_label = new base::DictionaryValue;
shared_with_me_label->SetString("label", "shared-with-me");
shared_with_me_label->SetString("scheme",
"http://schemas.google.com/g/2005/labels");
shared_with_me_label->SetString(
"term", "http://schemas.google.com/g/2005/labels#shared");
categories->Append(shared_with_me_label);
}
std::string escaped_resource_id = net::EscapePath(resource_id);
// Add "content" which sets the content URL.
base::DictionaryValue* content = new base::DictionaryValue;
content->SetString("src", "https://xxx/content/" + escaped_resource_id);
content->SetString("type", content_type);
new_entry->Set("content", content);
// Add "link" which sets the parent URL, the edit URL and the upload URL.
base::ListValue* links = new base::ListValue;
base::DictionaryValue* parent_link = new base::DictionaryValue;
if (parent_resource_id.empty())
parent_link->SetString("href", GetFakeLinkUrl(GetRootResourceId()).spec());
else
parent_link->SetString("href", GetFakeLinkUrl(parent_resource_id).spec());
parent_link->SetString("rel",
"http://schemas.google.com/docs/2007#parent");
links->Append(parent_link);
base::DictionaryValue* edit_link = new base::DictionaryValue;
edit_link->SetString("href", "https://xxx/edit/" + escaped_resource_id);
edit_link->SetString("rel", "edit");
links->Append(edit_link);
base::DictionaryValue* upload_link = new base::DictionaryValue;
upload_link->SetString("href", upload_url.spec());
upload_link->SetString("rel", kUploadUrlRel);
links->Append(upload_link);
const GURL share_url = net::AppendOrReplaceQueryParameter(
share_url_base_, "name", title);
base::DictionaryValue* share_link = new base::DictionaryValue;
upload_link->SetString("href", share_url.spec());
upload_link->SetString("rel", kShareUrlRel);
links->Append(share_link);
new_entry->Set("link", links);
AddNewChangestamp(new_entry.get());
UpdateETag(new_entry.get());
base::Time published_date =
base::Time() + base::TimeDelta::FromMilliseconds(++published_date_seq_);
new_entry->SetString(
"published.$t",
google_apis::util::FormatTimeAsString(published_date));
// If there are no entries, prepare an empty entry to add.
if (!resource_list_value_->HasKey("entry"))
resource_list_value_->Set("entry", new ListValue);
base::DictionaryValue* raw_new_entry = new_entry.release();
base::ListValue* entries = NULL;
if (resource_list_value_->GetList("entry", &entries))
entries->Append(raw_new_entry);
return raw_new_entry;
}
void FakeDriveService::GetResourceListInternal(
int64 start_changestamp,
const std::string& search_query,
const std::string& directory_resource_id,
int start_offset,
int max_results,
int* load_counter,
const GetResourceListCallback& callback) {
if (offline_) {
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback,
GDATA_NO_CONNECTION,
base::Passed(scoped_ptr<ResourceList>())));
return;
}
scoped_ptr<ResourceList> resource_list =
ResourceList::CreateFrom(*resource_list_value_);
// TODO(hashimoto): Drive API always provides largest changestamp. Remove this
// if-statement after API switch.
if (start_changestamp > 0 && start_offset == 0)
resource_list->set_largest_changestamp(largest_changestamp_);
// Filter out entries per parameters like |directory_resource_id| and
// |search_query|.
ScopedVector<ResourceEntry>* entries = resource_list->mutable_entries();
int num_entries_matched = 0;
for (size_t i = 0; i < entries->size();) {
ResourceEntry* entry = (*entries)[i];
bool should_exclude = false;
// If |directory_resource_id| is set, exclude the entry if it's not in
// the target directory.
if (!directory_resource_id.empty()) {
// Get the parent resource ID of the entry.
std::string parent_resource_id;
const google_apis::Link* parent_link =
entry->GetLinkByType(Link::LINK_PARENT);
if (parent_link) {
parent_resource_id =
net::UnescapeURLComponent(parent_link->href().ExtractFileName(),
net::UnescapeRule::URL_SPECIAL_CHARS);
}
if (directory_resource_id != parent_resource_id)
should_exclude = true;
}
// If |search_query| is set, exclude the entry if it does not contain the
// search query in the title.
if (!should_exclude && !search_query.empty() &&
!EntryMatchWithQuery(*entry, search_query)) {
should_exclude = true;
}
// If |start_changestamp| is set, exclude the entry if the
// changestamp is older than |largest_changestamp|.
// See https://developers.google.com/google-apps/documents-list/
// #retrieving_all_changes_since_a_given_changestamp
if (start_changestamp > 0 && entry->changestamp() < start_changestamp)
should_exclude = true;
// If the caller requests other list than change list by specifying
// zero-|start_changestamp|, exclude deleted entry from the result.
if (!start_changestamp && entry->deleted())
should_exclude = true;
// The entry matched the criteria for inclusion.
if (!should_exclude)
++num_entries_matched;
// If |start_offset| is set, exclude the entry if the entry is before the
// start index. <= instead of < as |num_entries_matched| was
// already incremented.
if (start_offset > 0 && num_entries_matched <= start_offset)
should_exclude = true;
if (should_exclude)
entries->erase(entries->begin() + i);
else
++i;
}
// If |max_results| is set, trim the entries if the number exceeded the max
// results.
if (max_results > 0 && entries->size() > static_cast<size_t>(max_results)) {
entries->erase(entries->begin() + max_results, entries->end());
// Adds the next URL.
// Here, we embed information which is needed for continuing the
// GetResourceList request in the next invocation into url query
// parameters.
GURL next_url(base::StringPrintf(
"http://localhost/?start-offset=%d&max-results=%d",
start_offset + max_results,
max_results));
if (start_changestamp > 0) {
next_url = net::AppendOrReplaceQueryParameter(
next_url, "changestamp",
base::Int64ToString(start_changestamp).c_str());
}
if (!search_query.empty()) {
next_url = net::AppendOrReplaceQueryParameter(
next_url, "q", search_query);
}
if (!directory_resource_id.empty()) {
next_url = net::AppendOrReplaceQueryParameter(
next_url, "parent", directory_resource_id);
}
Link* link = new Link;
link->set_type(Link::LINK_NEXT);
link->set_href(next_url);
resource_list->mutable_links()->push_back(link);
}
if (load_counter)
*load_counter += 1;
base::MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(callback, HTTP_SUCCESS, base::Passed(&resource_list)));
}
GURL FakeDriveService::GetNewUploadSessionUrl() {
return GURL("https://upload_session_url/" +
base::Int64ToString(next_upload_sequence_number_++));
}
} // namespace drive