Commit 78025162 authored by Mohamed Amir Yosef's avatar Mohamed Amir Yosef Committed by Commit Bot

[Sync::USS] Refactor BookmarkModelTypeProcessor

This CL extracts the logics for handling remote updates and committing
local updates into BookmarkRemoteUpdatesHandler and
BookmarkLocalChangesBuilder respectively.

This is to simplfy the BookmarkModelTypeProcessor.

Before this CL: processor scheduled metadata save only if there is a
change in the metadata without change in the model, because the model
would trigger the save in such cases.

After this CL: processor schedules metadata save every time  remote
updates are processed regardless from they entailed a change in the
model or not. This simplifies the code, and the bookmark storage
should squash subsequent save requests, so consecutive save requests
from the model and the processor should result in only one actually
save on disk.


Bug: 516866
Change-Id: I7918086b31b11f7a72804b152f23af2b059cca3d
Reviewed-on: https://chromium-review.googlesource.com/1124854
Commit-Queue: Mohamed Amir Yosef <mamir@chromium.org>
Reviewed-by: default avatarMikel Astiz <mastiz@chromium.org>
Cr-Commit-Position: refs/heads/master@{#572514}
parent f99b2ca3
...@@ -10,12 +10,16 @@ static_library("sync_bookmarks") { ...@@ -10,12 +10,16 @@ static_library("sync_bookmarks") {
"bookmark_change_processor.h", "bookmark_change_processor.h",
"bookmark_data_type_controller.cc", "bookmark_data_type_controller.cc",
"bookmark_data_type_controller.h", "bookmark_data_type_controller.h",
"bookmark_local_changes_builder.cc",
"bookmark_local_changes_builder.h",
"bookmark_model_associator.cc", "bookmark_model_associator.cc",
"bookmark_model_associator.h", "bookmark_model_associator.h",
"bookmark_model_observer_impl.cc", "bookmark_model_observer_impl.cc",
"bookmark_model_observer_impl.h", "bookmark_model_observer_impl.h",
"bookmark_model_type_processor.cc", "bookmark_model_type_processor.cc",
"bookmark_model_type_processor.h", "bookmark_model_type_processor.h",
"bookmark_remote_updates_handler.cc",
"bookmark_remote_updates_handler.h",
"bookmark_sync_service.cc", "bookmark_sync_service.cc",
"bookmark_sync_service.h", "bookmark_sync_service.h",
"synced_bookmark_tracker.cc", "synced_bookmark_tracker.cc",
...@@ -41,6 +45,7 @@ source_set("unit_tests") { ...@@ -41,6 +45,7 @@ source_set("unit_tests") {
"bookmark_data_type_controller_unittest.cc", "bookmark_data_type_controller_unittest.cc",
"bookmark_model_observer_impl_unittest.cc", "bookmark_model_observer_impl_unittest.cc",
"bookmark_model_type_processor_unittest.cc", "bookmark_model_type_processor_unittest.cc",
"bookmark_remote_updates_handler_unittest.cc",
"synced_bookmark_tracker_unittest.cc", "synced_bookmark_tracker_unittest.cc",
] ]
......
// Copyright 2018 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 "components/sync_bookmarks/bookmark_local_changes_builder.h"
#include <string>
#include <utility>
#include "base/strings/utf_string_conversions.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/sync/base/time.h"
#include "components/sync/protocol/bookmark_model_metadata.pb.h"
#include "components/sync_bookmarks/synced_bookmark_tracker.h"
namespace sync_bookmarks {
namespace {
sync_pb::EntitySpecifics SpecificsFromBookmarkNode(
const bookmarks::BookmarkNode* node) {
sync_pb::EntitySpecifics specifics;
sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
bm_specifics->set_url(node->url().spec());
// TODO(crbug.com/516866): Set the favicon.
bm_specifics->set_title(base::UTF16ToUTF8(node->GetTitle()));
bm_specifics->set_creation_time_us(
node->date_added().ToDeltaSinceWindowsEpoch().InMicroseconds());
bm_specifics->set_icon_url(node->icon_url() ? node->icon_url()->spec()
: std::string());
for (const std::pair<std::string, std::string>& pair :
*node->GetMetaInfoMap()) {
sync_pb::MetaInfo* meta_info = bm_specifics->add_meta_info();
meta_info->set_key(pair.first);
meta_info->set_value(pair.second);
}
return specifics;
}
} // namespace
BookmarkLocalChangesBuilder::BookmarkLocalChangesBuilder(
const SyncedBookmarkTracker* const bookmark_tracker)
: bookmark_tracker_(bookmark_tracker) {
DCHECK(bookmark_tracker);
}
std::vector<syncer::CommitRequestData>
BookmarkLocalChangesBuilder::BuildCommitRequests(size_t max_entries) const {
DCHECK(bookmark_tracker_);
const std::vector<const SyncedBookmarkTracker::Entity*>
entities_with_local_changes =
bookmark_tracker_->GetEntitiesWithLocalChanges(max_entries);
DCHECK_LE(entities_with_local_changes.size(), max_entries);
std::vector<syncer::CommitRequestData> commit_requests;
for (const SyncedBookmarkTracker::Entity* entity :
entities_with_local_changes) {
DCHECK(entity->IsUnsynced());
const sync_pb::EntityMetadata* metadata = entity->metadata();
syncer::CommitRequestData request;
syncer::EntityData data;
data.id = metadata->server_id();
data.creation_time = syncer::ProtoTimeToTime(metadata->creation_time());
data.modification_time =
syncer::ProtoTimeToTime(metadata->modification_time());
if (!metadata->is_deleted()) {
const bookmarks::BookmarkNode* node = entity->bookmark_node();
DCHECK(node);
const bookmarks::BookmarkNode* parent = node->parent();
const SyncedBookmarkTracker::Entity* parent_entity =
bookmark_tracker_->GetEntityForBookmarkNode(parent);
DCHECK(parent_entity);
data.parent_id = parent_entity->metadata()->server_id();
// TODO(crbug.com/516866): Double check that custom passphrase works well
// with this implementation, because:
// 1. NonBlockingTypeCommitContribution::AdjustCommitProto() clears the
// title out.
// 2. Bookmarks (maybe ancient legacy bookmarks only?) use/used |name| to
// encode the title.
data.is_folder = node->is_folder();
// TODO(crbug.com/516866): Set the non_unique_name similar to directory
// implementation.
// https://cs.chromium.org/chromium/src/components/sync/syncable/write_node.cc?l=41&rcl=1675007db1e0eb03417e81442688bb11cd181f58
data.non_unique_name = base::UTF16ToUTF8(node->GetTitle());
data.unique_position = parent_entity->metadata()->unique_position();
// In case of deletion, make an EntityData with empty specifics to
// indicate deletion.
data.specifics = SpecificsFromBookmarkNode(node);
}
request.entity = data.PassToPtr();
request.sequence_number = metadata->sequence_number();
request.base_version = metadata->server_version();
// Specifics hash has been computed in the tracker when this entity has been
// added/updated.
request.specifics_hash = metadata->specifics_hash();
commit_requests.push_back(std::move(request));
}
return commit_requests;
}
} // namespace sync_bookmarks
// Copyright 2018 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.
#ifndef COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_LOCAL_CHANGES_BUILDER_H_
#define COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_LOCAL_CHANGES_BUILDER_H_
#include <vector>
#include "components/sync/engine/non_blocking_sync_common.h"
namespace sync_bookmarks {
class SyncedBookmarkTracker;
class BookmarkLocalChangesBuilder {
public:
// |bookmark_tracker| must not be null and must outlive this object.
explicit BookmarkLocalChangesBuilder(
const SyncedBookmarkTracker* bookmark_tracker);
// Builds the commit requests list.
std::vector<syncer::CommitRequestData> BuildCommitRequests(
size_t max_entries) const;
private:
const SyncedBookmarkTracker* const bookmark_tracker_;
DISALLOW_COPY_AND_ASSIGN(BookmarkLocalChangesBuilder);
};
} // namespace sync_bookmarks
#endif // COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_LOCAL_CHANGES_BUILDER_H_
...@@ -7,138 +7,24 @@ ...@@ -7,138 +7,24 @@
#include <utility> #include <utility>
#include "base/callback.h" #include "base/callback.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h" #include "base/threading/thread_task_runner_handle.h"
#include "components/bookmarks/browser/bookmark_model.h" #include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h" #include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_utils.h" #include "components/bookmarks/browser/bookmark_utils.h"
#include "components/sync/base/model_type.h" #include "components/sync/base/model_type.h"
#include "components/sync/base/time.h"
#include "components/sync/engine/commit_queue.h" #include "components/sync/engine/commit_queue.h"
#include "components/sync/engine/model_type_processor_proxy.h" #include "components/sync/engine/model_type_processor_proxy.h"
#include "components/sync/model/data_type_activation_request.h" #include "components/sync/model/data_type_activation_request.h"
#include "components/sync/protocol/bookmark_model_metadata.pb.h" #include "components/sync/protocol/bookmark_model_metadata.pb.h"
#include "components/sync_bookmarks/bookmark_local_changes_builder.h"
#include "components/sync_bookmarks/bookmark_model_observer_impl.h" #include "components/sync_bookmarks/bookmark_model_observer_impl.h"
#include "components/sync_bookmarks/bookmark_remote_updates_handler.h"
#include "components/undo/bookmark_undo_utils.h" #include "components/undo/bookmark_undo_utils.h"
namespace sync_bookmarks { namespace sync_bookmarks {
namespace { namespace {
// The sync protocol identifies top-level entities by means of well-known tags,
// (aka server defined tags) which should not be confused with titles or client
// tags that aren't supported by bookmarks (at the time of writing). Each tag
// corresponds to a singleton instance of a particular top-level node in a
// user's share; the tags are consistent across users. The tags allow us to
// locate the specific folders whose contents we care about synchronizing,
// without having to do a lookup by name or path. The tags should not be made
// user-visible. For example, the tag "bookmark_bar" represents the permanent
// node for bookmarks bar in Chrome. The tag "other_bookmarks" represents the
// permanent folder Other Bookmarks in Chrome.
//
// It is the responsibility of something upstream (at time of writing, the sync
// server) to create these tagged nodes when initializing sync for the first
// time for a user. Thus, once the backend finishes initializing, the
// ProfileSyncService can rely on the presence of tagged nodes.
const char kBookmarkBarTag[] = "bookmark_bar";
const char kMobileBookmarksTag[] = "synced_bookmarks";
const char kOtherBookmarksTag[] = "other_bookmarks";
// Id is created by concatenating the specifics field number and the server tag
// similar to LookbackServerEntity::CreateId() that uses
// GetSpecificsFieldNumberFromModelType() to compute the field number.
static const char kBookmarksRootId[] = "32904_google_chrome_bookmarks";
// |sync_entity| must contain a bookmark specifics.
// Metainfo entries must have unique keys.
bookmarks::BookmarkNode::MetaInfoMap GetBookmarkMetaInfo(
const syncer::EntityData& sync_entity) {
const sync_pb::BookmarkSpecifics& specifics =
sync_entity.specifics.bookmark();
bookmarks::BookmarkNode::MetaInfoMap meta_info_map;
for (const sync_pb::MetaInfo& meta_info : specifics.meta_info()) {
meta_info_map[meta_info.key()] = meta_info.value();
}
DCHECK_EQ(static_cast<size_t>(specifics.meta_info_size()),
meta_info_map.size());
return meta_info_map;
}
// Creates a bookmark node under the given parent node from the given sync node.
// Returns the newly created node. |sync_entity| must contain a bookmark
// specifics with Metainfo entries having unique keys.
const bookmarks::BookmarkNode* CreateBookmarkNode(
const syncer::EntityData& sync_entity,
const bookmarks::BookmarkNode* parent,
bookmarks::BookmarkModel* model,
int index) {
DCHECK(parent);
DCHECK(model);
const sync_pb::BookmarkSpecifics& specifics =
sync_entity.specifics.bookmark();
bookmarks::BookmarkNode::MetaInfoMap metainfo =
GetBookmarkMetaInfo(sync_entity);
if (sync_entity.is_folder) {
return model->AddFolderWithMetaInfo(
parent, index, base::UTF8ToUTF16(specifics.title()), &metainfo);
}
// 'creation_time_us' was added in M24. Assume a time of 0 means now.
const int64_t create_time_us = specifics.creation_time_us();
base::Time create_time =
(create_time_us == 0)
? base::Time::Now()
: base::Time::FromDeltaSinceWindowsEpoch(
// Use FromDeltaSinceWindowsEpoch because create_time_us has
// always used the Windows epoch.
base::TimeDelta::FromMicroseconds(create_time_us));
return model->AddURLWithCreationTimeAndMetaInfo(
parent, index, base::UTF8ToUTF16(specifics.title()),
GURL(specifics.url()), create_time, &metainfo);
// TODO(crbug.com/516866): Add the favicon related code.
}
// Check whether an incoming specifics represent a valid bookmark or not.
// |is_folder| is whether this specifics is for a folder or not.
// Folders and tomstones entail different validation conditions.
bool IsValidBookmark(const sync_pb::BookmarkSpecifics& specifics,
bool is_folder) {
if (specifics.ByteSize() == 0) {
DLOG(ERROR) << "Invalid bookmark: empty specifics.";
return false;
}
if (is_folder) {
return true;
}
if (!GURL(specifics.url()).is_valid()) {
DLOG(ERROR) << "Invalid bookmark: invalid url in the specifics.";
return false;
}
return true;
}
sync_pb::EntitySpecifics SpecificsFromBookmarkNode(
const bookmarks::BookmarkNode* node) {
sync_pb::EntitySpecifics specifics;
sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
bm_specifics->set_url(node->url().spec());
// TODO(crbug.com/516866): Set the favicon.
bm_specifics->set_title(base::UTF16ToUTF8(node->GetTitle()));
bm_specifics->set_creation_time_us(
node->date_added().ToDeltaSinceWindowsEpoch().InMicroseconds());
bm_specifics->set_icon_url(node->icon_url() ? node->icon_url()->spec()
: std::string());
for (const std::pair<std::string, std::string>& pair :
*node->GetMetaInfoMap()) {
sync_pb::MetaInfo* meta_info = bm_specifics->add_meta_info();
meta_info->set_key(pair.first);
meta_info->set_value(pair.second);
}
return specifics;
}
class ScopedRemoteUpdateBookmarks { class ScopedRemoteUpdateBookmarks {
public: public:
// |bookmark_model|, |bookmark_undo_service| and |observer| must not be null // |bookmark_model|, |bookmark_undo_service| and |observer| must not be null
...@@ -178,6 +64,7 @@ class ScopedRemoteUpdateBookmarks { ...@@ -178,6 +64,7 @@ class ScopedRemoteUpdateBookmarks {
DISALLOW_COPY_AND_ASSIGN(ScopedRemoteUpdateBookmarks); DISALLOW_COPY_AND_ASSIGN(ScopedRemoteUpdateBookmarks);
}; };
} // namespace } // namespace
BookmarkModelTypeProcessor::BookmarkModelTypeProcessor( BookmarkModelTypeProcessor::BookmarkModelTypeProcessor(
...@@ -213,8 +100,9 @@ void BookmarkModelTypeProcessor::GetLocalChanges( ...@@ -213,8 +100,9 @@ void BookmarkModelTypeProcessor::GetLocalChanges(
size_t max_entries, size_t max_entries,
const GetLocalChangesCallback& callback) { const GetLocalChangesCallback& callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
BookmarkLocalChangesBuilder builder(bookmark_tracker_.get());
std::vector<syncer::CommitRequestData> local_changes = std::vector<syncer::CommitRequestData> local_changes =
BuildCommitRequestsForLocalChanges(max_entries); builder.BuildCommitRequests(max_entries);
callback.Run(std::move(local_changes)); callback.Run(std::move(local_changes));
} }
...@@ -256,36 +144,11 @@ void BookmarkModelTypeProcessor::OnUpdateReceived( ...@@ -256,36 +144,11 @@ void BookmarkModelTypeProcessor::OnUpdateReceived(
ScopedRemoteUpdateBookmarks update_bookmarks( ScopedRemoteUpdateBookmarks update_bookmarks(
bookmark_model_, bookmark_undo_service_, bookmark_model_observer_.get()); bookmark_model_, bookmark_undo_service_, bookmark_model_observer_.get());
BookmarkRemoteUpdatesHandler updates_handler(bookmark_model_,
for (const syncer::UpdateResponseData* update : ReorderUpdates(updates)) { bookmark_tracker_.get());
const syncer::EntityData& update_entity = update->entity.value(); updates_handler.Process(updates);
// TODO(crbug.com/516866): Check |update_entity| for sanity. // Schedule save just in case one is needed.
// 1. Has bookmark specifics or no specifics in case of delete. schedule_save_closure_.Run();
// 2. All meta info entries in the specifics have unique keys.
const SyncedBookmarkTracker::Entity* tracked_entity =
bookmark_tracker_->GetEntityForSyncId(update_entity.id);
if (update_entity.is_deleted()) {
ProcessRemoteDelete(update_entity, tracked_entity);
continue;
}
if (!tracked_entity) {
ProcessRemoteCreate(*update);
continue;
}
// Ignore changes to the permanent nodes (e.g. bookmarks bar). We only care
// about their children.
if (bookmark_model_->is_permanent_node(tracked_entity->bookmark_node())) {
continue;
}
ProcessRemoteUpdate(*update, tracked_entity);
}
}
// static
std::vector<const syncer::UpdateResponseData*>
BookmarkModelTypeProcessor::ReorderUpdatesForTest(
const syncer::UpdateResponseDataList& updates) {
return ReorderUpdates(updates);
} }
const SyncedBookmarkTracker* BookmarkModelTypeProcessor::GetTrackerForTest() const SyncedBookmarkTracker* BookmarkModelTypeProcessor::GetTrackerForTest()
...@@ -293,216 +156,6 @@ const SyncedBookmarkTracker* BookmarkModelTypeProcessor::GetTrackerForTest() ...@@ -293,216 +156,6 @@ const SyncedBookmarkTracker* BookmarkModelTypeProcessor::GetTrackerForTest()
return bookmark_tracker_.get(); return bookmark_tracker_.get();
} }
// static
std::vector<const syncer::UpdateResponseData*>
BookmarkModelTypeProcessor::ReorderUpdates(
const syncer::UpdateResponseDataList& updates) {
// TODO(crbug.com/516866): This is a very simple (hacky) reordering algorithm
// that assumes no folders exist except the top level permanent ones. This
// should be fixed before enabling USS for bookmarks.
std::vector<const syncer::UpdateResponseData*> ordered_updates;
for (const syncer::UpdateResponseData& update : updates) {
const syncer::EntityData& update_entity = update.entity.value();
if (update_entity.parent_id == "0") {
continue;
}
if (update_entity.parent_id == kBookmarksRootId) {
ordered_updates.push_back(&update);
}
}
for (const syncer::UpdateResponseData& update : updates) {
const syncer::EntityData& update_entity = update.entity.value();
// Deletions should come last.
if (update_entity.is_deleted()) {
continue;
}
if (update_entity.parent_id != "0" &&
update_entity.parent_id != kBookmarksRootId) {
ordered_updates.push_back(&update);
}
}
// Now add deletions.
for (const syncer::UpdateResponseData& update : updates) {
const syncer::EntityData& update_entity = update.entity.value();
if (!update_entity.is_deleted()) {
continue;
}
if (update_entity.parent_id != "0" &&
update_entity.parent_id != kBookmarksRootId) {
ordered_updates.push_back(&update);
}
}
return ordered_updates;
}
void BookmarkModelTypeProcessor::ProcessRemoteCreate(
const syncer::UpdateResponseData& update) {
// Because the Synced Bookmarks node can be created server side, it's possible
// it'll arrive at the client as an update. In that case it won't have been
// associated at startup, the GetChromeNodeFromSyncId call above will return
// null, and we won't detect it as a permanent node, resulting in us trying to
// create it here (which will fail). Therefore, we add special logic here just
// to detect the Synced Bookmarks folder.
const syncer::EntityData& update_entity = update.entity.value();
DCHECK(!update_entity.is_deleted());
if (update_entity.parent_id == kBookmarksRootId) {
// Associate permanent folders.
// TODO(crbug.com/516866): Method documentation says this method should be
// used in initial sync only. Make sure this is the case.
AssociatePermanentFolder(update);
return;
}
if (!IsValidBookmark(update_entity.specifics.bookmark(),
update_entity.is_folder)) {
// Ignore creations with invalid specifics.
DLOG(ERROR) << "Couldn't add bookmark with an invalid specifics.";
return;
}
const bookmarks::BookmarkNode* parent_node = GetParentNode(update_entity);
if (!parent_node) {
// If we cannot find the parent, we can do nothing.
DLOG(ERROR) << "Could not find parent of node being added."
<< " Node title: " << update_entity.specifics.bookmark().title()
<< ", parent id = " << update_entity.parent_id;
return;
}
// TODO(crbug.com/516866): This code appends the code to the very end of the
// list of the children by assigning the index to the
// parent_node->child_count(). It should instead compute the exact using the
// unique position information of the new node as well as the siblings.
const bookmarks::BookmarkNode* bookmark_node = CreateBookmarkNode(
update_entity, parent_node, bookmark_model_, parent_node->child_count());
if (!bookmark_node) {
// We ignore bookmarks we can't add.
DLOG(ERROR) << "Failed to create bookmark node with title "
<< update_entity.specifics.bookmark().title() << " and url "
<< update_entity.specifics.bookmark().url();
return;
}
bookmark_tracker_->Add(update_entity.id, bookmark_node,
update.response_version, update_entity.creation_time,
update_entity.unique_position,
update_entity.specifics);
}
void BookmarkModelTypeProcessor::ProcessRemoteUpdate(
const syncer::UpdateResponseData& update,
const SyncedBookmarkTracker::Entity* tracked_entity) {
const syncer::EntityData& update_entity = update.entity.value();
// Can only update existing nodes.
DCHECK(tracked_entity);
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
// Must not be a deletion.
DCHECK(!update_entity.is_deleted());
if (!IsValidBookmark(update_entity.specifics.bookmark(),
update_entity.is_folder)) {
// Ignore updates with invalid specifics.
DLOG(ERROR) << "Couldn't update bookmark with an invalid specifics.";
return;
}
if (tracked_entity->IsUnsynced()) {
// TODO(crbug.com/516866): Handle conflict resolution.
return;
}
if (tracked_entity->MatchesData(update_entity)) {
bookmark_tracker_->Update(update_entity.id, update.response_version,
update_entity.modification_time,
update_entity.specifics);
// Since there is no change in the model data, we should trigger data
// persistence here to save latest metadata.
schedule_save_closure_.Run();
return;
}
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
if (update_entity.is_folder != node->is_folder()) {
DLOG(ERROR) << "Could not update node. Remote node is a "
<< (update_entity.is_folder ? "folder" : "bookmark")
<< " while local node is a "
<< (node->is_folder() ? "folder" : "bookmark");
return;
}
const sync_pb::BookmarkSpecifics& specifics =
update_entity.specifics.bookmark();
if (!update_entity.is_folder) {
bookmark_model_->SetURL(node, GURL(specifics.url()));
}
bookmark_model_->SetTitle(node, base::UTF8ToUTF16(specifics.title()));
// TODO(crbug.com/516866): Add the favicon related code.
bookmark_model_->SetNodeMetaInfoMap(node, GetBookmarkMetaInfo(update_entity));
bookmark_tracker_->Update(update_entity.id, update.response_version,
update_entity.modification_time,
update_entity.specifics);
// TODO(crbug.com/516866): Handle reparenting.
// TODO(crbug.com/516866): Handle the case of moving the bookmark to a new
// position under the same parent (i.e. change in the unique position)
}
void BookmarkModelTypeProcessor::ProcessRemoteDelete(
const syncer::EntityData& update_entity,
const SyncedBookmarkTracker::Entity* tracked_entity) {
DCHECK(update_entity.is_deleted());
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
// Handle corner cases first.
if (tracked_entity == nullptr) {
// Local entity doesn't exist and update is tombstone.
DLOG(WARNING) << "Received remote delete for a non-existing item.";
return;
}
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
// Ignore changes to the permanent top-level nodes. We only care about
// their children.
if (bookmark_model_->is_permanent_node(node)) {
return;
}
// TODO(crbug.com/516866): Allow deletions of non-empty direcoties if makes
// sense, and recursively delete children.
if (node->child_count() > 0) {
DLOG(WARNING) << "Trying to delete a non-empty folder.";
return;
}
bookmark_model_->Remove(node);
bookmark_tracker_->Remove(update_entity.id);
}
const bookmarks::BookmarkNode* BookmarkModelTypeProcessor::GetParentNode(
const syncer::EntityData& update_entity) const {
const SyncedBookmarkTracker::Entity* parent_entity =
bookmark_tracker_->GetEntityForSyncId(update_entity.parent_id);
if (!parent_entity) {
return nullptr;
}
return parent_entity->bookmark_node();
}
void BookmarkModelTypeProcessor::AssociatePermanentFolder(
const syncer::UpdateResponseData& update) {
const syncer::EntityData& update_entity = update.entity.value();
DCHECK_EQ(update_entity.parent_id, kBookmarksRootId);
const bookmarks::BookmarkNode* permanent_node = nullptr;
if (update_entity.server_defined_unique_tag == kBookmarkBarTag) {
permanent_node = bookmark_model_->bookmark_bar_node();
} else if (update_entity.server_defined_unique_tag == kOtherBookmarksTag) {
permanent_node = bookmark_model_->other_node();
} else if (update_entity.server_defined_unique_tag == kMobileBookmarksTag) {
permanent_node = bookmark_model_->mobile_node();
}
if (permanent_node != nullptr) {
bookmark_tracker_->Add(update_entity.id, permanent_node,
update.response_version, update_entity.creation_time,
update_entity.unique_position,
update_entity.specifics);
}
}
std::string BookmarkModelTypeProcessor::EncodeSyncMetadata() const { std::string BookmarkModelTypeProcessor::EncodeSyncMetadata() const {
std::string metadata_str; std::string metadata_str;
if (bookmark_tracker_) { if (bookmark_tracker_) {
...@@ -652,63 +305,6 @@ void BookmarkModelTypeProcessor::NudgeForCommitIfNeeded() { ...@@ -652,63 +305,6 @@ void BookmarkModelTypeProcessor::NudgeForCommitIfNeeded() {
} }
} }
std::vector<syncer::CommitRequestData>
BookmarkModelTypeProcessor::BuildCommitRequestsForLocalChanges(
size_t max_entries) {
DCHECK(bookmark_tracker_);
const std::vector<const SyncedBookmarkTracker::Entity*>
entities_with_local_changes =
bookmark_tracker_->GetEntitiesWithLocalChanges(max_entries);
DCHECK_LE(entities_with_local_changes.size(), max_entries);
std::vector<syncer::CommitRequestData> commit_requests;
for (const SyncedBookmarkTracker::Entity* entity :
entities_with_local_changes) {
DCHECK(entity->IsUnsynced());
const sync_pb::EntityMetadata* metadata = entity->metadata();
syncer::CommitRequestData request;
syncer::EntityData data;
data.id = metadata->server_id();
data.creation_time = syncer::ProtoTimeToTime(metadata->creation_time());
data.modification_time =
syncer::ProtoTimeToTime(metadata->modification_time());
if (!metadata->is_deleted()) {
const bookmarks::BookmarkNode* node = entity->bookmark_node();
DCHECK(node);
const bookmarks::BookmarkNode* parent = node->parent();
const SyncedBookmarkTracker::Entity* parent_entity =
bookmark_tracker_->GetEntityForBookmarkNode(parent);
DCHECK(parent_entity);
data.parent_id = parent_entity->metadata()->server_id();
// TODO(crbug.com/516866): Double check that custom passphrase works well
// with this implementation, because:
// 1. NonBlockingTypeCommitContribution::AdjustCommitProto() clears the
// title out.
// 2. Bookmarks (maybe ancient legacy bookmarks only?) use/used |name| to
// encode the title.
data.is_folder = node->is_folder();
// TODO(crbug.com/516866): Set the non_unique_name similar to directory
// implementation.
// https://cs.chromium.org/chromium/src/components/sync/syncable/write_node.cc?l=41&rcl=1675007db1e0eb03417e81442688bb11cd181f58
data.non_unique_name = base::UTF16ToUTF8(node->GetTitle());
data.unique_position = parent_entity->metadata()->unique_position();
// In case of deletion, make an EntityData with empty specifics to
// indicate deletion.
data.specifics = SpecificsFromBookmarkNode(node);
}
request.entity = data.PassToPtr();
request.sequence_number = metadata->sequence_number();
request.base_version = metadata->server_version();
// Specifics hash has been computed in the tracker when this entity has been
// added/updated.
request.specifics_hash = metadata->specifics_hash();
commit_requests.push_back(std::move(request));
}
return commit_requests;
}
void BookmarkModelTypeProcessor::StartTrackingMetadata( void BookmarkModelTypeProcessor::StartTrackingMetadata(
std::vector<NodeMetadataPair> nodes_metadata, std::vector<NodeMetadataPair> nodes_metadata,
std::unique_ptr<sync_pb::ModelTypeState> model_type_state) { std::unique_ptr<sync_pb::ModelTypeState> model_type_state) {
......
...@@ -22,7 +22,6 @@ class BookmarkUndoService; ...@@ -22,7 +22,6 @@ class BookmarkUndoService;
namespace bookmarks { namespace bookmarks {
class BookmarkModel; class BookmarkModel;
class BookmarkNode;
} }
namespace sync_bookmarks { namespace sync_bookmarks {
...@@ -71,10 +70,6 @@ class BookmarkModelTypeProcessor : public syncer::ModelTypeProcessor, ...@@ -71,10 +70,6 @@ class BookmarkModelTypeProcessor : public syncer::ModelTypeProcessor,
const base::RepeatingClosure& schedule_save_closure, const base::RepeatingClosure& schedule_save_closure,
bookmarks::BookmarkModel* model); bookmarks::BookmarkModel* model);
// Public for testing.
static std::vector<const syncer::UpdateResponseData*> ReorderUpdatesForTest(
const syncer::UpdateResponseDataList& updates);
const SyncedBookmarkTracker* GetTrackerForTest() const; const SyncedBookmarkTracker* GetTrackerForTest() const;
base::WeakPtr<syncer::ModelTypeControllerDelegate> GetWeakPtr(); base::WeakPtr<syncer::ModelTypeControllerDelegate> GetWeakPtr();
...@@ -82,51 +77,6 @@ class BookmarkModelTypeProcessor : public syncer::ModelTypeProcessor, ...@@ -82,51 +77,6 @@ class BookmarkModelTypeProcessor : public syncer::ModelTypeProcessor,
private: private:
SEQUENCE_CHECKER(sequence_checker_); SEQUENCE_CHECKER(sequence_checker_);
// Reorders incoming updates such that parent creation is before child
// creation and child deletion is before parent deletion, and deletions should
// come last. The returned pointers point to the elements in the original
// |updates|.
static std::vector<const syncer::UpdateResponseData*> ReorderUpdates(
const syncer::UpdateResponseDataList& updates);
// Given a remote update entity, it returns the parent bookmark node of the
// corresponding node. It returns null if the parent node cannot be found.
const bookmarks::BookmarkNode* GetParentNode(
const syncer::EntityData& update_entity) const;
// Processes a remote creation of a bookmark node.
// 1. For permanent folders, they are only registered in |bookmark_tracker_|.
// 2. If the nodes parent cannot be found, the remote creation update is
// ignored.
// 3. Otherwise, a new node is created in the local bookmark model and
// registered in |bookmark_tracker_|.
void ProcessRemoteCreate(const syncer::UpdateResponseData& update);
// Processes a remote update of a bookmark node. |update| must not be a
// deletion, and the server_id must be already tracked, otherwise, it is a
// creation that gets handeled in ProcessRemoteCreate(). |tracked_entity| is
// the tracked entity for that server_id. It is passed as a dependency instead
// of performing a lookup inside ProcessRemoteUpdate() to avoid wasting CPU
// cycles for doing another lookup (this code runs on the UI thread).
void ProcessRemoteUpdate(const syncer::UpdateResponseData& update,
const SyncedBookmarkTracker::Entity* tracked_entity);
// Process a remote delete of a bookmark node. |update_entity| must not be a
// deletion. |tracked_entity| is the tracked entity for that server_id. It is
// passed as a dependency instead of performing a lookup inside
// ProcessRemoteDelete() to avoid wasting CPU cycles for doing another lookup
// (this code runs on the UI thread).
void ProcessRemoteDelete(const syncer::EntityData& update_entity,
const SyncedBookmarkTracker::Entity* tracked_entity);
// Associates the permanent bookmark folders with the corresponding server
// side ids and registers the association in |bookmark_tracker_|.
// |update_entity| must contain server_defined_unique_tag that is used to
// determine the corresponding permanent node. All permanent nodes are assumed
// to be directly children nodes of |kBookmarksRootId|. This method is used in
// the initial sync cycle only.
void AssociatePermanentFolder(const syncer::UpdateResponseData& update);
// If preconditions are met, inform sync that we are ready to connect. // If preconditions are met, inform sync that we are ready to connect.
void ConnectIfReady(); void ConnectIfReady();
...@@ -135,10 +85,6 @@ class BookmarkModelTypeProcessor : public syncer::ModelTypeProcessor, ...@@ -135,10 +85,6 @@ class BookmarkModelTypeProcessor : public syncer::ModelTypeProcessor,
// entities. // entities.
void NudgeForCommitIfNeeded(); void NudgeForCommitIfNeeded();
// Builds the commit requests list.
std::vector<syncer::CommitRequestData> BuildCommitRequestsForLocalChanges(
size_t max_entries);
// Instantiates the required objects to track metadata and starts observing // Instantiates the required objects to track metadata and starts observing
// changes from the bookmark model. // changes from the bookmark model.
void StartTrackingMetadata( void StartTrackingMetadata(
......
...@@ -25,9 +25,6 @@ namespace sync_bookmarks { ...@@ -25,9 +25,6 @@ namespace sync_bookmarks {
namespace { namespace {
// The parent tag for children of the root entity. Entities with this parent are
// referred to as top level enities.
const char kRootParentTag[] = "0";
const char kBookmarkBarTag[] = "bookmark_bar"; const char kBookmarkBarTag[] = "bookmark_bar";
const char kBookmarkBarId[] = "bookmark_bar_id"; const char kBookmarkBarId[] = "bookmark_bar_id";
const char kBookmarksRootId[] = "32904_google_chrome_bookmarks"; const char kBookmarksRootId[] = "32904_google_chrome_bookmarks";
...@@ -119,12 +116,6 @@ syncer::UpdateResponseData CreateTombstone(const std::string& server_id) { ...@@ -119,12 +116,6 @@ syncer::UpdateResponseData CreateTombstone(const std::string& server_id) {
return response_data; return response_data;
} }
syncer::UpdateResponseData CreateBookmarkRootUpdateData() {
return CreateUpdateData({syncer::ModelTypeToRootTag(syncer::BOOKMARKS),
std::string(), std::string(), kRootParentTag,
syncer::ModelTypeToRootTag(syncer::BOOKMARKS)});
}
class TestSyncClient : public syncer::FakeSyncClient { class TestSyncClient : public syncer::FakeSyncClient {
public: public:
explicit TestSyncClient(bookmarks::BookmarkModel* bookmark_model) explicit TestSyncClient(bookmarks::BookmarkModel* bookmark_model)
...@@ -169,38 +160,6 @@ class BookmarkModelTypeProcessorTest : public testing::Test { ...@@ -169,38 +160,6 @@ class BookmarkModelTypeProcessorTest : public testing::Test {
BookmarkModelTypeProcessor processor_; BookmarkModelTypeProcessor processor_;
}; };
TEST(BookmarkModelTypeProcessorReorderUpdatesTest, ShouldIgnoreRootNodes) {
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkRootUpdateData());
std::vector<const syncer::UpdateResponseData*> ordered_updates =
BookmarkModelTypeProcessor::ReorderUpdatesForTest(updates);
// Root node update should be filtered out.
EXPECT_THAT(ordered_updates.size(), Eq(0U));
}
// TODO(crbug.com/516866): This should change to cover the general case of
// parents before children for non-deletions, and another test should be added
// for children before parents for deletions.
TEST(BookmarkModelTypeProcessorReorderUpdatesTest,
ShouldPlacePermanentNodesFirstForNonDeletions) {
const std::string kNode1Id = "node1";
const std::string kNode2Id = "node2";
syncer::UpdateResponseDataList updates;
updates.push_back(CreateUpdateData(
{kNode1Id, std::string(), std::string(), kNode2Id, std::string()}));
updates.push_back(CreateUpdateData({kNode2Id, std::string(), std::string(),
kBookmarksRootId, kBookmarkBarTag}));
std::vector<const syncer::UpdateResponseData*> ordered_updates =
BookmarkModelTypeProcessor::ReorderUpdatesForTest(updates);
// No update should be dropped.
ASSERT_THAT(ordered_updates.size(), Eq(2U));
// Updates should be ordered such that parent node update comes first.
EXPECT_THAT(ordered_updates[0]->entity.value().id, Eq(kNode2Id));
EXPECT_THAT(ordered_updates[1]->entity.value().id, Eq(kNode1Id));
}
TEST_F(BookmarkModelTypeProcessorTest, ShouldUpdateModelAfterRemoteCreation) { TEST_F(BookmarkModelTypeProcessorTest, ShouldUpdateModelAfterRemoteCreation) {
syncer::UpdateResponseDataList updates; syncer::UpdateResponseDataList updates;
// Add update for the permanent folder "Bookmarks bar". // Add update for the permanent folder "Bookmarks bar".
...@@ -219,9 +178,6 @@ TEST_F(BookmarkModelTypeProcessorTest, ShouldUpdateModelAfterRemoteCreation) { ...@@ -219,9 +178,6 @@ TEST_F(BookmarkModelTypeProcessorTest, ShouldUpdateModelAfterRemoteCreation) {
bookmark_model()->bookmark_bar_node(); bookmark_model()->bookmark_bar_node();
EXPECT_TRUE(bookmarkbar->empty()); EXPECT_TRUE(bookmarkbar->empty());
// Save will be scheduled in the model upon model change. No save should be
// scheduled from the processor.
EXPECT_CALL(*schedule_save_closure(), Run()).Times(0);
processor()->OnUpdateReceived(CreateDummyModelTypeState(), updates); processor()->OnUpdateReceived(CreateDummyModelTypeState(), updates);
ASSERT_THAT(bookmarkbar->GetChild(0), NotNull()); ASSERT_THAT(bookmarkbar->GetChild(0), NotNull());
...@@ -255,9 +211,6 @@ TEST_F(BookmarkModelTypeProcessorTest, ShouldUpdateModelAfterRemoteUpdate) { ...@@ -255,9 +211,6 @@ TEST_F(BookmarkModelTypeProcessorTest, ShouldUpdateModelAfterRemoteUpdate) {
CreateUpdateData({kNodeId, kNewTitle, kNewUrl, kBookmarkBarId, CreateUpdateData({kNodeId, kNewTitle, kNewUrl, kBookmarkBarId,
/*server_tag=*/std::string()})); /*server_tag=*/std::string()}));
// Save will be scheduled in the model upon model change. No save should be
// scheduled from the processor.
EXPECT_CALL(*schedule_save_closure(), Run()).Times(0);
processor()->OnUpdateReceived(CreateDummyModelTypeState(), updates); processor()->OnUpdateReceived(CreateDummyModelTypeState(), updates);
// Check if the bookmark has been updated properly. // Check if the bookmark has been updated properly.
...@@ -347,9 +300,6 @@ TEST_F(BookmarkModelTypeProcessorTest, ShouldUpdateModelAfterRemoteDelete) { ...@@ -347,9 +300,6 @@ TEST_F(BookmarkModelTypeProcessorTest, ShouldUpdateModelAfterRemoteDelete) {
updates.push_back(CreateTombstone(kTitle1Id)); updates.push_back(CreateTombstone(kTitle1Id));
updates.push_back(CreateTombstone(kFolder1Id)); updates.push_back(CreateTombstone(kFolder1Id));
// Save will be scheduled in the model upon model change. No save should be
// scheduled from the processor.
EXPECT_CALL(*schedule_save_closure(), Run()).Times(0);
processor()->OnUpdateReceived(CreateDummyModelTypeState(), updates); processor()->OnUpdateReceived(CreateDummyModelTypeState(), updates);
// The structure should be // The structure should be
......
// Copyright 2018 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 "components/sync_bookmarks/bookmark_remote_updates_handler.h"
#include "base/strings/utf_string_conversions.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
namespace sync_bookmarks {
namespace {
// The sync protocol identifies top-level entities by means of well-known tags,
// (aka server defined tags) which should not be confused with titles or client
// tags that aren't supported by bookmarks (at the time of writing). Each tag
// corresponds to a singleton instance of a particular top-level node in a
// user's share; the tags are consistent across users. The tags allow us to
// locate the specific folders whose contents we care about synchronizing,
// without having to do a lookup by name or path. The tags should not be made
// user-visible. For example, the tag "bookmark_bar" represents the permanent
// node for bookmarks bar in Chrome. The tag "other_bookmarks" represents the
// permanent folder Other Bookmarks in Chrome.
//
// It is the responsibility of something upstream (at time of writing, the sync
// server) to create these tagged nodes when initializing sync for the first
// time for a user. Thus, once the backend finishes initializing, the
// ProfileSyncService can rely on the presence of tagged nodes.
const char kBookmarkBarTag[] = "bookmark_bar";
const char kMobileBookmarksTag[] = "synced_bookmarks";
const char kOtherBookmarksTag[] = "other_bookmarks";
// Id is created by concatenating the specifics field number and the server tag
// similar to LookbackServerEntity::CreateId() that uses
// GetSpecificsFieldNumberFromModelType() to compute the field number.
const char kBookmarksRootId[] = "32904_google_chrome_bookmarks";
// |sync_entity| must contain a bookmark specifics.
// Metainfo entries must have unique keys.
bookmarks::BookmarkNode::MetaInfoMap GetBookmarkMetaInfo(
const syncer::EntityData& sync_entity) {
const sync_pb::BookmarkSpecifics& specifics =
sync_entity.specifics.bookmark();
bookmarks::BookmarkNode::MetaInfoMap meta_info_map;
for (const sync_pb::MetaInfo& meta_info : specifics.meta_info()) {
meta_info_map[meta_info.key()] = meta_info.value();
}
DCHECK_EQ(static_cast<size_t>(specifics.meta_info_size()),
meta_info_map.size());
return meta_info_map;
}
// Creates a bookmark node under the given parent node from the given sync node.
// Returns the newly created node. |sync_entity| must contain a bookmark
// specifics with Metainfo entries having unique keys.
const bookmarks::BookmarkNode* CreateBookmarkNode(
const syncer::EntityData& sync_entity,
const bookmarks::BookmarkNode* parent,
bookmarks::BookmarkModel* model,
int index) {
DCHECK(parent);
DCHECK(model);
const sync_pb::BookmarkSpecifics& specifics =
sync_entity.specifics.bookmark();
bookmarks::BookmarkNode::MetaInfoMap metainfo =
GetBookmarkMetaInfo(sync_entity);
if (sync_entity.is_folder) {
return model->AddFolderWithMetaInfo(
parent, index, base::UTF8ToUTF16(specifics.title()), &metainfo);
}
// 'creation_time_us' was added in M24. Assume a time of 0 means now.
const int64_t create_time_us = specifics.creation_time_us();
base::Time create_time =
(create_time_us == 0)
? base::Time::Now()
: base::Time::FromDeltaSinceWindowsEpoch(
// Use FromDeltaSinceWindowsEpoch because create_time_us has
// always used the Windows epoch.
base::TimeDelta::FromMicroseconds(create_time_us));
return model->AddURLWithCreationTimeAndMetaInfo(
parent, index, base::UTF8ToUTF16(specifics.title()),
GURL(specifics.url()), create_time, &metainfo);
// TODO(crbug.com/516866): Add the favicon related code.
}
// Check whether an incoming specifics represent a valid bookmark or not.
// |is_folder| is whether this specifics is for a folder or not.
// Folders and tomstones entail different validation conditions.
bool IsValidBookmark(const sync_pb::BookmarkSpecifics& specifics,
bool is_folder) {
if (specifics.ByteSize() == 0) {
DLOG(ERROR) << "Invalid bookmark: empty specifics.";
return false;
}
if (is_folder) {
return true;
}
if (!GURL(specifics.url()).is_valid()) {
DLOG(ERROR) << "Invalid bookmark: invalid url in the specifics.";
return false;
}
return true;
}
} // namespace
BookmarkRemoteUpdatesHandler::BookmarkRemoteUpdatesHandler(
bookmarks::BookmarkModel* bookmark_model,
SyncedBookmarkTracker* bookmark_tracker)
: bookmark_model_(bookmark_model), bookmark_tracker_(bookmark_tracker) {
DCHECK(bookmark_model);
DCHECK(bookmark_tracker);
}
void BookmarkRemoteUpdatesHandler::Process(
const syncer::UpdateResponseDataList& updates) {
for (const syncer::UpdateResponseData* update : ReorderUpdates(updates)) {
const syncer::EntityData& update_entity = update->entity.value();
// TODO(crbug.com/516866): Check |update_entity| for sanity.
// 1. Has bookmark specifics or no specifics in case of delete.
// 2. All meta info entries in the specifics have unique keys.
const SyncedBookmarkTracker::Entity* tracked_entity =
bookmark_tracker_->GetEntityForSyncId(update_entity.id);
if (update_entity.is_deleted()) {
ProcessRemoteDelete(update_entity, tracked_entity);
continue;
}
if (!tracked_entity) {
ProcessRemoteCreate(*update);
continue;
}
// Ignore changes to the permanent nodes (e.g. bookmarks bar). We only care
// about their children.
if (bookmark_model_->is_permanent_node(tracked_entity->bookmark_node())) {
continue;
}
ProcessRemoteUpdate(*update, tracked_entity);
}
}
// static
std::vector<const syncer::UpdateResponseData*>
BookmarkRemoteUpdatesHandler::ReorderUpdatesForTest(
const syncer::UpdateResponseDataList& updates) {
return ReorderUpdates(updates);
}
// static
std::vector<const syncer::UpdateResponseData*>
BookmarkRemoteUpdatesHandler::ReorderUpdates(
const syncer::UpdateResponseDataList& updates) {
// TODO(crbug.com/516866): This is a very simple (hacky) reordering algorithm
// that assumes no folders exist except the top level permanent ones. This
// should be fixed before enabling USS for bookmarks.
std::vector<const syncer::UpdateResponseData*> ordered_updates;
for (const syncer::UpdateResponseData& update : updates) {
const syncer::EntityData& update_entity = update.entity.value();
if (update_entity.parent_id == "0") {
continue;
}
if (update_entity.parent_id == kBookmarksRootId) {
ordered_updates.push_back(&update);
}
}
for (const syncer::UpdateResponseData& update : updates) {
const syncer::EntityData& update_entity = update.entity.value();
// Deletions should come last.
if (update_entity.is_deleted()) {
continue;
}
if (update_entity.parent_id != "0" &&
update_entity.parent_id != kBookmarksRootId) {
ordered_updates.push_back(&update);
}
}
// Now add deletions.
for (const syncer::UpdateResponseData& update : updates) {
const syncer::EntityData& update_entity = update.entity.value();
if (!update_entity.is_deleted()) {
continue;
}
if (update_entity.parent_id != "0" &&
update_entity.parent_id != kBookmarksRootId) {
ordered_updates.push_back(&update);
}
}
return ordered_updates;
}
void BookmarkRemoteUpdatesHandler::ProcessRemoteCreate(
const syncer::UpdateResponseData& update) {
// Because the Synced Bookmarks node can be created server side, it's possible
// it'll arrive at the client as an update. In that case it won't have been
// associated at startup, the GetChromeNodeFromSyncId call above will return
// null, and we won't detect it as a permanent node, resulting in us trying to
// create it here (which will fail). Therefore, we add special logic here just
// to detect the Synced Bookmarks folder.
const syncer::EntityData& update_entity = update.entity.value();
DCHECK(!update_entity.is_deleted());
if (update_entity.parent_id == kBookmarksRootId) {
// Associate permanent folders.
// TODO(crbug.com/516866): Method documentation says this method should be
// used in initial sync only. Make sure this is the case.
AssociatePermanentFolder(update);
return;
}
if (!IsValidBookmark(update_entity.specifics.bookmark(),
update_entity.is_folder)) {
// Ignore creations with invalid specifics.
DLOG(ERROR) << "Couldn't add bookmark with an invalid specifics.";
return;
}
const bookmarks::BookmarkNode* parent_node = GetParentNode(update_entity);
if (!parent_node) {
// If we cannot find the parent, we can do nothing.
DLOG(ERROR) << "Could not find parent of node being added."
<< " Node title: " << update_entity.specifics.bookmark().title()
<< ", parent id = " << update_entity.parent_id;
return;
}
// TODO(crbug.com/516866): This code appends the code to the very end of the
// list of the children by assigning the index to the
// parent_node->child_count(). It should instead compute the exact using the
// unique position information of the new node as well as the siblings.
const bookmarks::BookmarkNode* bookmark_node = CreateBookmarkNode(
update_entity, parent_node, bookmark_model_, parent_node->child_count());
if (!bookmark_node) {
// We ignore bookmarks we can't add.
DLOG(ERROR) << "Failed to create bookmark node with title "
<< update_entity.specifics.bookmark().title() << " and url "
<< update_entity.specifics.bookmark().url();
return;
}
bookmark_tracker_->Add(update_entity.id, bookmark_node,
update.response_version, update_entity.creation_time,
update_entity.unique_position,
update_entity.specifics);
}
void BookmarkRemoteUpdatesHandler::ProcessRemoteUpdate(
const syncer::UpdateResponseData& update,
const SyncedBookmarkTracker::Entity* tracked_entity) {
const syncer::EntityData& update_entity = update.entity.value();
// Can only update existing nodes.
DCHECK(tracked_entity);
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
// Must not be a deletion.
DCHECK(!update_entity.is_deleted());
if (!IsValidBookmark(update_entity.specifics.bookmark(),
update_entity.is_folder)) {
// Ignore updates with invalid specifics.
DLOG(ERROR) << "Couldn't update bookmark with an invalid specifics.";
return;
}
if (tracked_entity->IsUnsynced()) {
// TODO(crbug.com/516866): Handle conflict resolution.
return;
}
if (tracked_entity->MatchesData(update_entity)) {
bookmark_tracker_->Update(update_entity.id, update.response_version,
update_entity.modification_time,
update_entity.specifics);
return;
}
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
if (update_entity.is_folder != node->is_folder()) {
DLOG(ERROR) << "Could not update node. Remote node is a "
<< (update_entity.is_folder ? "folder" : "bookmark")
<< " while local node is a "
<< (node->is_folder() ? "folder" : "bookmark");
return;
}
const sync_pb::BookmarkSpecifics& specifics =
update_entity.specifics.bookmark();
if (!update_entity.is_folder) {
bookmark_model_->SetURL(node, GURL(specifics.url()));
}
bookmark_model_->SetTitle(node, base::UTF8ToUTF16(specifics.title()));
// TODO(crbug.com/516866): Add the favicon related code.
bookmark_model_->SetNodeMetaInfoMap(node, GetBookmarkMetaInfo(update_entity));
bookmark_tracker_->Update(update_entity.id, update.response_version,
update_entity.modification_time,
update_entity.specifics);
// TODO(crbug.com/516866): Handle reparenting.
// TODO(crbug.com/516866): Handle the case of moving the bookmark to a new
// position under the same parent (i.e. change in the unique position)
}
void BookmarkRemoteUpdatesHandler::ProcessRemoteDelete(
const syncer::EntityData& update_entity,
const SyncedBookmarkTracker::Entity* tracked_entity) {
DCHECK(update_entity.is_deleted());
DCHECK_EQ(tracked_entity,
bookmark_tracker_->GetEntityForSyncId(update_entity.id));
// Handle corner cases first.
if (tracked_entity == nullptr) {
// Local entity doesn't exist and update is tombstone.
DLOG(WARNING) << "Received remote delete for a non-existing item.";
return;
}
const bookmarks::BookmarkNode* node = tracked_entity->bookmark_node();
// Ignore changes to the permanent top-level nodes. We only care about
// their children.
if (bookmark_model_->is_permanent_node(node)) {
return;
}
// TODO(crbug.com/516866): Allow deletions of non-empty direcoties if makes
// sense, and recursively delete children.
if (node->child_count() > 0) {
DLOG(WARNING) << "Trying to delete a non-empty folder.";
return;
}
bookmark_model_->Remove(node);
bookmark_tracker_->Remove(update_entity.id);
}
const bookmarks::BookmarkNode* BookmarkRemoteUpdatesHandler::GetParentNode(
const syncer::EntityData& update_entity) const {
const SyncedBookmarkTracker::Entity* parent_entity =
bookmark_tracker_->GetEntityForSyncId(update_entity.parent_id);
if (!parent_entity) {
return nullptr;
}
return parent_entity->bookmark_node();
}
void BookmarkRemoteUpdatesHandler::AssociatePermanentFolder(
const syncer::UpdateResponseData& update) {
const syncer::EntityData& update_entity = update.entity.value();
DCHECK_EQ(update_entity.parent_id, kBookmarksRootId);
const bookmarks::BookmarkNode* permanent_node = nullptr;
if (update_entity.server_defined_unique_tag == kBookmarkBarTag) {
permanent_node = bookmark_model_->bookmark_bar_node();
} else if (update_entity.server_defined_unique_tag == kOtherBookmarksTag) {
permanent_node = bookmark_model_->other_node();
} else if (update_entity.server_defined_unique_tag == kMobileBookmarksTag) {
permanent_node = bookmark_model_->mobile_node();
}
if (permanent_node != nullptr) {
bookmark_tracker_->Add(update_entity.id, permanent_node,
update.response_version, update_entity.creation_time,
update_entity.unique_position,
update_entity.specifics);
}
}
} // namespace sync_bookmarks
// Copyright 2018 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.
#ifndef COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_REMOTE_UPDATES_HANDLER_H_
#define COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_REMOTE_UPDATES_HANDLER_H_
#include <vector>
#include "components/sync/engine/non_blocking_sync_common.h"
#include "components/sync_bookmarks/synced_bookmark_tracker.h"
namespace bookmarks {
class BookmarkModel;
class BookmarkNode;
} // namespace bookmarks
namespace sync_bookmarks {
// Responsible for processing remote updates received from the sync server.
class BookmarkRemoteUpdatesHandler {
public:
// |bookmark_model| and |bookmark_tracker| must not be null and most outlive
// this object.
BookmarkRemoteUpdatesHandler(bookmarks::BookmarkModel* bookmark_model,
SyncedBookmarkTracker* bookmark_tracker);
// Processes the updates received from the sync server in |updates| and
// updates the |bookmark_model_| and |bookmark_tracker_| accordingly.
void Process(const syncer::UpdateResponseDataList& updates);
// Public for testing.
static std::vector<const syncer::UpdateResponseData*> ReorderUpdatesForTest(
const syncer::UpdateResponseDataList& updates);
private:
// Reorders incoming updates such that parent creation is before child
// creation and child deletion is before parent deletion, and deletions should
// come last. The returned pointers point to the elements in the original
// |updates|.
static std::vector<const syncer::UpdateResponseData*> ReorderUpdates(
const syncer::UpdateResponseDataList& updates);
// Given a remote update entity, it returns the parent bookmark node of the
// corresponding node. It returns null if the parent node cannot be found.
const bookmarks::BookmarkNode* GetParentNode(
const syncer::EntityData& update_entity) const;
// Processes a remote creation of a bookmark node.
// 1. For permanent folders, they are only registered in |bookmark_tracker_|.
// 2. If the nodes parent cannot be found, the remote creation update is
// ignored.
// 3. Otherwise, a new node is created in the local bookmark model and
// registered in |bookmark_tracker_|.
void ProcessRemoteCreate(const syncer::UpdateResponseData& update);
// Processes a remote update of a bookmark node. |update| must not be a
// deletion, and the server_id must be already tracked, otherwise, it is a
// creation that gets handeled in ProcessRemoteCreate(). |tracked_entity| is
// the tracked entity for that server_id. It is passed as a dependency instead
// of performing a lookup inside ProcessRemoteUpdate() to avoid wasting CPU
// cycles for doing another lookup (this code runs on the UI thread).
void ProcessRemoteUpdate(const syncer::UpdateResponseData& update,
const SyncedBookmarkTracker::Entity* tracked_entity);
// Process a remote delete of a bookmark node. |update_entity| must not be a
// deletion. |tracked_entity| is the tracked entity for that server_id. It is
// passed as a dependency instead of performing a lookup inside
// ProcessRemoteDelete() to avoid wasting CPU cycles for doing another lookup
// (this code runs on the UI thread).
void ProcessRemoteDelete(const syncer::EntityData& update_entity,
const SyncedBookmarkTracker::Entity* tracked_entity);
// Associates the permanent bookmark folders with the corresponding server
// side ids and registers the association in |bookmark_tracker_|.
// |update_entity| must contain server_defined_unique_tag that is used to
// determine the corresponding permanent node. All permanent nodes are assumed
// to be directly children nodes of |kBookmarksRootId|. This method is used in
// the initial sync cycle only.
void AssociatePermanentFolder(const syncer::UpdateResponseData& update);
bookmarks::BookmarkModel* const bookmark_model_;
SyncedBookmarkTracker* const bookmark_tracker_;
DISALLOW_COPY_AND_ASSIGN(BookmarkRemoteUpdatesHandler);
};
} // namespace sync_bookmarks
#endif // COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_REMOTE_UPDATES_HANDLER_H_
// Copyright 2018 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 "components/sync_bookmarks/bookmark_remote_updates_handler.h"
#include <string>
#include "components/sync/base/model_type.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::Eq;
namespace sync_bookmarks {
namespace {
// The parent tag for children of the root entity. Entities with this parent are
// referred to as top level enities.
const char kRootParentTag[] = "0";
const char kBookmarkBarTag[] = "bookmark_bar";
const char kBookmarksRootId[] = "32904_google_chrome_bookmarks";
struct BookmarkInfo {
std::string server_id;
std::string title;
std::string url; // empty for folders.
std::string parent_id;
std::string server_tag;
};
syncer::UpdateResponseData CreateUpdateData(const BookmarkInfo& bookmark_info) {
syncer::EntityData data;
data.id = bookmark_info.server_id;
data.parent_id = bookmark_info.parent_id;
data.server_defined_unique_tag = bookmark_info.server_tag;
sync_pb::BookmarkSpecifics* bookmark_specifics =
data.specifics.mutable_bookmark();
bookmark_specifics->set_title(bookmark_info.title);
if (bookmark_info.url.empty()) {
data.is_folder = true;
} else {
bookmark_specifics->set_url(bookmark_info.url);
}
syncer::UpdateResponseData response_data;
response_data.entity = data.PassToPtr();
// Similar to what's done in the loopback_server.
response_data.response_version = 0;
return response_data;
}
syncer::UpdateResponseData CreateBookmarkRootUpdateData() {
return CreateUpdateData({syncer::ModelTypeToRootTag(syncer::BOOKMARKS),
std::string(), std::string(), kRootParentTag,
syncer::ModelTypeToRootTag(syncer::BOOKMARKS)});
}
TEST(BookmarkRemoteUpdatesHandlerReorderUpdatesTest, ShouldIgnoreRootNodes) {
syncer::UpdateResponseDataList updates;
updates.push_back(CreateBookmarkRootUpdateData());
std::vector<const syncer::UpdateResponseData*> ordered_updates =
BookmarkRemoteUpdatesHandler::ReorderUpdatesForTest(updates);
// Root node update should be filtered out.
EXPECT_THAT(ordered_updates.size(), Eq(0U));
}
// TODO(crbug.com/516866): This should change to cover the general case of
// parents before children for non-deletions, and another test should be added
// for children before parents for deletions.
TEST(BookmarkRemoteUpdatesHandlerReorderUpdatesTest,
ShouldPlacePermanentNodesFirstForNonDeletions) {
const std::string kNode1Id = "node1";
const std::string kNode2Id = "node2";
syncer::UpdateResponseDataList updates;
updates.push_back(CreateUpdateData(
{kNode1Id, std::string(), std::string(), kNode2Id, std::string()}));
updates.push_back(CreateUpdateData({kNode2Id, std::string(), std::string(),
kBookmarksRootId, kBookmarkBarTag}));
std::vector<const syncer::UpdateResponseData*> ordered_updates =
BookmarkRemoteUpdatesHandler::ReorderUpdatesForTest(updates);
// No update should be dropped.
ASSERT_THAT(ordered_updates.size(), Eq(2U));
// Updates should be ordered such that parent node update comes first.
EXPECT_THAT(ordered_updates[0]->entity.value().id, Eq(kNode2Id));
EXPECT_THAT(ordered_updates[1]->entity.value().id, Eq(kNode1Id));
}
} // namespace
} // namespace sync_bookmarks
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment