| // Copyright (c) 2006-2009 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/sync/glue/model_associator.h" |
| |
| #include <stack> |
| |
| #include "base/hash_tables.h" |
| #include "base/message_loop.h" |
| #include "base/task.h" |
| #include "chrome/browser/bookmarks/bookmark_model.h" |
| #include "chrome/browser/sync/engine/syncapi.h" |
| #include "chrome/browser/sync/profile_sync_service.h" |
| |
| namespace browser_sync { |
| |
| // The sync protocol identifies top-level entities by means of well-known tags, |
| // which should not be confused with titles. 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. |
| // |
| // TODO(ncarter): Pull these tags from an external protocol specification |
| // rather than hardcoding them here. |
| static const char kBookmarkBarTag[] = "bookmark_bar"; |
| static const char kOtherBookmarksTag[] = "other_bookmarks"; |
| |
| // Bookmark comparer for map of bookmark nodes. |
| class BookmarkComparer { |
| public: |
| // Compares the two given nodes and returns whether node1 should appear |
| // before node2 in strict weak ordering. |
| bool operator()(const BookmarkNode* node1, |
| const BookmarkNode* node2) const { |
| DCHECK(node1); |
| DCHECK(node2); |
| |
| // Keep folder nodes before non-folder nodes. |
| if (node1->is_folder() != node2->is_folder()) |
| return node1->is_folder(); |
| |
| int result = node1->GetTitle().compare(node2->GetTitle()); |
| if (result != 0) |
| return result < 0; |
| |
| return node1->GetURL() < node2->GetURL(); |
| } |
| }; |
| |
| // Provides the following abstraction: given a parent bookmark node, find best |
| // matching child node for many sync nodes. |
| class BookmarkNodeFinder { |
| public: |
| // Creates an instance with the given parent bookmark node. |
| explicit BookmarkNodeFinder(const BookmarkNode* parent_node); |
| |
| // Finds best matching node for the given sync node. |
| // Returns the matching node if one exists; NULL otherwise. If a matching |
| // node is found, it's removed for further matches. |
| const BookmarkNode* FindBookmarkNode(const sync_api::BaseNode& sync_node); |
| |
| private: |
| typedef std::multiset<const BookmarkNode*, BookmarkComparer> BookmarkNodesSet; |
| |
| const BookmarkNode* parent_node_; |
| BookmarkNodesSet child_nodes_; |
| |
| DISALLOW_COPY_AND_ASSIGN(BookmarkNodeFinder); |
| }; |
| |
| BookmarkNodeFinder::BookmarkNodeFinder(const BookmarkNode* parent_node) |
| : parent_node_(parent_node) { |
| for (int i = 0; i < parent_node_->GetChildCount(); ++i) { |
| child_nodes_.insert(parent_node_->GetChild(i)); |
| } |
| } |
| |
| const BookmarkNode* BookmarkNodeFinder::FindBookmarkNode( |
| const sync_api::BaseNode& sync_node) { |
| // Create a bookmark node from the given sync node. |
| BookmarkNode temp_node(sync_node.GetURL()); |
| temp_node.SetTitle(sync_node.GetTitle()); |
| if (sync_node.GetIsFolder()) |
| temp_node.set_type(BookmarkNode::FOLDER); |
| else |
| temp_node.set_type(BookmarkNode::URL); |
| |
| const BookmarkNode* result = NULL; |
| BookmarkNodesSet::iterator iter = child_nodes_.find(&temp_node); |
| if (iter != child_nodes_.end()) { |
| result = *iter; |
| // Remove the matched node so we don't match with it again. |
| child_nodes_.erase(iter); |
| } |
| |
| return result; |
| } |
| |
| // Helper class to build an index of bookmark nodes by their IDs. |
| class BookmarkNodeIdIndex { |
| public: |
| BookmarkNodeIdIndex() { } |
| ~BookmarkNodeIdIndex() { } |
| |
| // Adds the given bookmark node and all its descendants to the ID index. |
| // Does nothing if node is NULL. |
| void AddAll(const BookmarkNode* node); |
| |
| // Finds the bookmark node with the given ID. |
| // Returns NULL if none exists with the given id. |
| const BookmarkNode* Find(int64 id) const; |
| |
| // Returns the count of nodes in the index. |
| size_t count() const { return node_index_.size(); } |
| |
| private: |
| typedef base::hash_map<int64, const BookmarkNode*> BookmarkIdMap; |
| // Map that holds nodes indexed by their ids. |
| BookmarkIdMap node_index_; |
| |
| DISALLOW_COPY_AND_ASSIGN(BookmarkNodeIdIndex); |
| }; |
| |
| void BookmarkNodeIdIndex::AddAll(const BookmarkNode* node) { |
| if (!node) |
| return; |
| |
| node_index_[node->id()] = node; |
| |
| if (!node->is_folder()) |
| return; |
| |
| for (int i = 0; i < node->GetChildCount(); ++i) |
| AddAll(node->GetChild(i)); |
| } |
| |
| const BookmarkNode* BookmarkNodeIdIndex::Find(int64 id) const { |
| BookmarkIdMap::const_iterator iter = node_index_.find(id); |
| return iter == node_index_.end() ? NULL : iter->second; |
| } |
| |
| ModelAssociator::ModelAssociator(ProfileSyncService* sync_service) |
| : sync_service_(sync_service), |
| task_pending_(false) { |
| DCHECK(sync_service_); |
| } |
| |
| void ModelAssociator::ClearAll() { |
| id_map_.clear(); |
| id_map_inverse_.clear(); |
| dirty_associations_sync_ids_.clear(); |
| } |
| |
| int64 ModelAssociator::GetSyncIdFromBookmarkId(int64 node_id) const { |
| BookmarkIdToSyncIdMap::const_iterator iter = id_map_.find(node_id); |
| return iter == id_map_.end() ? sync_api::kInvalidId : iter->second; |
| } |
| |
| const BookmarkNode* ModelAssociator::GetBookmarkNodeFromSyncId(int64 sync_id) { |
| SyncIdToBookmarkNodeMap::const_iterator iter = id_map_inverse_.find(sync_id); |
| return iter == id_map_inverse_.end() ? NULL : iter->second; |
| } |
| |
| bool ModelAssociator::InitSyncNodeFromBookmarkId( |
| int64 node_id, |
| sync_api::BaseNode* sync_node) { |
| DCHECK(sync_node); |
| int64 sync_id = GetSyncIdFromBookmarkId(node_id); |
| if (sync_id == sync_api::kInvalidId) |
| return false; |
| if (!sync_node->InitByIdLookup(sync_id)) |
| return false; |
| DCHECK(sync_node->GetId() == sync_id); |
| return true; |
| } |
| |
| void ModelAssociator::Associate(const BookmarkNode* node, int64 sync_id) { |
| int64 node_id = node->id(); |
| DCHECK_NE(sync_id, sync_api::kInvalidId); |
| DCHECK(id_map_.find(node_id) == id_map_.end()); |
| DCHECK(id_map_inverse_.find(sync_id) == id_map_inverse_.end()); |
| id_map_[node_id] = sync_id; |
| id_map_inverse_[sync_id] = node; |
| dirty_associations_sync_ids_.insert(sync_id); |
| PostPersistAssociationsTask(); |
| } |
| |
| void ModelAssociator::Disassociate(int64 sync_id) { |
| SyncIdToBookmarkNodeMap::iterator iter = id_map_inverse_.find(sync_id); |
| if (iter == id_map_inverse_.end()) |
| return; |
| id_map_.erase(iter->second->id()); |
| id_map_inverse_.erase(iter); |
| dirty_associations_sync_ids_.erase(sync_id); |
| } |
| |
| bool ModelAssociator::BookmarkModelHasUserCreatedNodes() const { |
| BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); |
| DCHECK(model->IsLoaded()); |
| return model->GetBookmarkBarNode()->GetChildCount() > 0 || |
| model->other_node()->GetChildCount() > 0; |
| } |
| |
| bool ModelAssociator::SyncModelHasUserCreatedNodes() { |
| int64 bookmark_bar_sync_id; |
| if (!GetSyncIdForTaggedNode(kBookmarkBarTag,&bookmark_bar_sync_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| int64 other_bookmarks_sync_id; |
| if (!GetSyncIdForTaggedNode(kOtherBookmarksTag, &other_bookmarks_sync_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| |
| sync_api::ReadTransaction trans( |
| sync_service_->backend()->GetUserShareHandle()); |
| |
| sync_api::ReadNode bookmark_bar_node(&trans); |
| if (!bookmark_bar_node.InitByIdLookup(bookmark_bar_sync_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| |
| sync_api::ReadNode other_bookmarks_node(&trans); |
| if (!other_bookmarks_node.InitByIdLookup(other_bookmarks_sync_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| |
| // Sync model has user created nodes if either one of the permanent nodes |
| // has children. |
| return bookmark_bar_node.GetFirstChildId() != sync_api::kInvalidId || |
| other_bookmarks_node.GetFirstChildId() != sync_api::kInvalidId; |
| } |
| |
| bool ModelAssociator::NodesMatch(const BookmarkNode* bookmark, |
| const sync_api::BaseNode* sync_node) const { |
| if (bookmark->GetTitle() != sync_node->GetTitle()) |
| return false; |
| if (bookmark->is_folder() != sync_node->GetIsFolder()) |
| return false; |
| if (bookmark->is_url()) { |
| if (bookmark->GetURL() != sync_node->GetURL()) |
| return false; |
| } |
| // Don't compare favicons here, because they are not really |
| // user-updated and we don't have versioning information -- a site changing |
| // its favicon shouldn't result in a bookmark mismatch. |
| return true; |
| } |
| |
| bool ModelAssociator::AssociateTaggedPermanentNode( |
| const BookmarkNode* permanent_node, const std::string&tag) { |
| // Do nothing if |permanent_node| is already initialized and associated. |
| int64 sync_id = GetSyncIdFromBookmarkId(permanent_node->id()); |
| if (sync_id != sync_api::kInvalidId) |
| return true; |
| if (!GetSyncIdForTaggedNode(tag, &sync_id)) |
| return false; |
| |
| Associate(permanent_node, sync_id); |
| return true; |
| } |
| |
| bool ModelAssociator::GetSyncIdForTaggedNode(const std::string& tag, |
| int64* sync_id) { |
| sync_api::ReadTransaction trans( |
| sync_service_->backend()->GetUserShareHandle()); |
| sync_api::ReadNode sync_node(&trans); |
| if (!sync_node.InitByTagLookup(tag.c_str())) |
| return false; |
| *sync_id = sync_node.GetId(); |
| return true; |
| } |
| |
| bool ModelAssociator::AssociateModels() { |
| // Try to load model associations from persisted associations first. If that |
| // succeeds, we don't need to run the complex model matching algorithm. |
| if (LoadAssociations()) |
| return true; |
| |
| ClearAll(); |
| |
| // We couldn't load model associations from persisted associations. So build |
| // them. |
| return BuildAssociations(); |
| } |
| |
| bool ModelAssociator::BuildAssociations() { |
| // Algorithm description: |
| // Match up the roots and recursively do the following: |
| // * For each sync node for the current sync parent node, find the best |
| // matching bookmark node under the corresponding bookmark parent node. |
| // If no matching node is found, create a new bookmark node in the same |
| // position as the corresponding sync node. |
| // If a matching node is found, update the properties of it from the |
| // corresponding sync node. |
| // * When all children sync nodes are done, add the extra children bookmark |
| // nodes to the sync parent node. |
| // |
| // This algorithm will do a good job of merging when folder names are a good |
| // indicator of the two folders being the same. It will handle reordering and |
| // new node addition very well (without creating duplicates). |
| // This algorithm will not do well if the folder name has changes but the |
| // children under them are all the same. |
| |
| BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); |
| DCHECK(model->IsLoaded()); |
| |
| // To prime our association, we associate the top-level nodes, Bookmark Bar |
| // and Other Bookmarks. |
| if (!AssociateTaggedPermanentNode(model->other_node(), kOtherBookmarksTag)) { |
| sync_service_->OnUnrecoverableError(); |
| LOG(ERROR) << "Server did not create top-level nodes. Possibly we " |
| << "are running against an out-of-date server?"; |
| return false; |
| } |
| if (!AssociateTaggedPermanentNode(model->GetBookmarkBarNode(), |
| kBookmarkBarTag)) { |
| sync_service_->OnUnrecoverableError(); |
| LOG(ERROR) << "Server did not create top-level nodes. Possibly we " |
| << "are running against an out-of-date server?"; |
| return false; |
| } |
| int64 bookmark_bar_sync_id = GetSyncIdFromBookmarkId( |
| model->GetBookmarkBarNode()->id()); |
| DCHECK(bookmark_bar_sync_id != sync_api::kInvalidId); |
| int64 other_bookmarks_sync_id = GetSyncIdFromBookmarkId( |
| model->other_node()->id()); |
| DCHECK(other_bookmarks_sync_id != sync_api::kInvalidId); |
| |
| std::stack<int64> dfs_stack; |
| dfs_stack.push(other_bookmarks_sync_id); |
| dfs_stack.push(bookmark_bar_sync_id); |
| |
| sync_api::WriteTransaction trans( |
| sync_service_->backend()->GetUserShareHandle()); |
| |
| while (!dfs_stack.empty()) { |
| int64 sync_parent_id = dfs_stack.top(); |
| dfs_stack.pop(); |
| |
| sync_api::ReadNode sync_parent(&trans); |
| if (!sync_parent.InitByIdLookup(sync_parent_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| // Only folder nodes are pushed on to the stack. |
| DCHECK(sync_parent.GetIsFolder()); |
| |
| const BookmarkNode* parent_node = GetBookmarkNodeFromSyncId(sync_parent_id); |
| DCHECK(parent_node->is_folder()); |
| |
| BookmarkNodeFinder node_finder(parent_node); |
| |
| int index = 0; |
| int64 sync_child_id = sync_parent.GetFirstChildId(); |
| while (sync_child_id != sync_api::kInvalidId) { |
| sync_api::WriteNode sync_child_node(&trans); |
| if (!sync_child_node.InitByIdLookup(sync_child_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| |
| const BookmarkNode* child_node = NULL; |
| child_node = node_finder.FindBookmarkNode(sync_child_node); |
| if (child_node) { |
| model->Move(child_node, parent_node, index); |
| // Set the favicon for bookmark node from sync node or vice versa. |
| if (ChangeProcessor::SetBookmarkFavicon(&sync_child_node, |
| child_node, sync_service_->profile())) { |
| ChangeProcessor::SetSyncNodeFavicon(child_node, model, |
| &sync_child_node); |
| } |
| } else { |
| // Create a new bookmark node for the sync node. |
| child_node = ChangeProcessor::CreateBookmarkNode(&sync_child_node, |
| parent_node, model, index); |
| } |
| Associate(child_node, sync_child_id); |
| if (sync_child_node.GetIsFolder()) |
| dfs_stack.push(sync_child_id); |
| |
| sync_child_id = sync_child_node.GetSuccessorId(); |
| ++index; |
| } |
| |
| // At this point all the children nodes of the parent sync node have |
| // corresponding children in the parent bookmark node and they are all in |
| // the right positions: from 0 to index - 1. |
| // So the children starting from index in the parent bookmark node are the |
| // ones that are not present in the parent sync node. So create them. |
| for (int i = index; i < parent_node->GetChildCount(); ++i) { |
| sync_child_id = ChangeProcessor::CreateSyncNode(parent_node, model, i, |
| &trans, this, sync_service_); |
| if (parent_node->GetChild(i)->is_folder()) |
| dfs_stack.push(sync_child_id); |
| } |
| } |
| return true; |
| } |
| |
| void ModelAssociator::PostPersistAssociationsTask() { |
| // No need to post a task if a task is already pending. |
| if (task_pending_) |
| return; |
| task_pending_ = true; |
| MessageLoop::current()->PostTask( |
| FROM_HERE, |
| NewRunnableMethod(this, &ModelAssociator::PersistAssociations)); |
| } |
| |
| void ModelAssociator::PersistAssociations() { |
| DCHECK(task_pending_); |
| task_pending_ = false; |
| |
| // If there are no dirty associations we have nothing to do. We handle this |
| // explicity instead of letting the for loop do it to avoid creating a write |
| // transaction in this case. |
| if (dirty_associations_sync_ids_.empty()) { |
| DCHECK(id_map_.empty()); |
| DCHECK(id_map_inverse_.empty()); |
| return; |
| } |
| |
| sync_api::WriteTransaction trans( |
| sync_service_->backend()->GetUserShareHandle()); |
| DirtyAssociationsSyncIds::iterator iter; |
| for (iter = dirty_associations_sync_ids_.begin(); |
| iter != dirty_associations_sync_ids_.end(); |
| ++iter) { |
| int64 sync_id = *iter; |
| sync_api::WriteNode sync_node(&trans); |
| if (!sync_node.InitByIdLookup(sync_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return; |
| } |
| const BookmarkNode* node = GetBookmarkNodeFromSyncId(sync_id); |
| if (node) |
| sync_node.SetExternalId(node->id()); |
| else |
| NOTREACHED(); |
| } |
| dirty_associations_sync_ids_.clear(); |
| } |
| |
| bool ModelAssociator::LoadAssociations() { |
| BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); |
| DCHECK(model->IsLoaded()); |
| // If the bookmarks changed externally, our previous associations may not be |
| // valid; so return false. |
| if (model->file_changed()) |
| return false; |
| |
| // Our persisted associations should be valid. Try to populate id association |
| // maps using persisted associations. Note that the unit tests will |
| // create the tagged nodes on demand, and the order in which we probe for |
| // them here will impact their positional ordering in that case. |
| int64 bookmark_bar_id; |
| if (!GetSyncIdForTaggedNode(kBookmarkBarTag, &bookmark_bar_id)) { |
| // We should always be able to find the permanent nodes. |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| int64 other_bookmarks_id; |
| if (!GetSyncIdForTaggedNode(kOtherBookmarksTag, &other_bookmarks_id)) { |
| // We should always be able to find the permanent nodes. |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| |
| // Build a bookmark node ID index since we are going to repeatedly search for |
| // bookmark nodes by their IDs. |
| BookmarkNodeIdIndex id_index; |
| id_index.AddAll(model->GetBookmarkBarNode()); |
| id_index.AddAll(model->other_node()); |
| |
| std::stack<int64> dfs_stack; |
| dfs_stack.push(other_bookmarks_id); |
| dfs_stack.push(bookmark_bar_id); |
| |
| sync_api::ReadTransaction trans( |
| sync_service_->backend()->GetUserShareHandle()); |
| |
| // Count total number of nodes in sync model so that we can compare that |
| // with the total number of nodes in the bookmark model. |
| size_t sync_node_count = 0; |
| while (!dfs_stack.empty()) { |
| int64 parent_id = dfs_stack.top(); |
| dfs_stack.pop(); |
| ++sync_node_count; |
| sync_api::ReadNode sync_parent(&trans); |
| if (!sync_parent.InitByIdLookup(parent_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| |
| int64 external_id = sync_parent.GetExternalId(); |
| if (external_id == 0) |
| return false; |
| |
| const BookmarkNode* node = id_index.Find(external_id); |
| if (!node) |
| return false; |
| |
| // Don't try to call NodesMatch on permanent nodes like bookmark bar and |
| // other bookmarks. They are not expected to match. |
| if (node != model->GetBookmarkBarNode() && |
| node != model->other_node() && |
| !NodesMatch(node, &sync_parent)) |
| return false; |
| |
| Associate(node, sync_parent.GetId()); |
| |
| // Add all children of the current node to the stack. |
| int64 child_id = sync_parent.GetFirstChildId(); |
| while (child_id != sync_api::kInvalidId) { |
| dfs_stack.push(child_id); |
| sync_api::ReadNode child_node(&trans); |
| if (!child_node.InitByIdLookup(child_id)) { |
| sync_service_->OnUnrecoverableError(); |
| return false; |
| } |
| child_id = child_node.GetSuccessorId(); |
| } |
| } |
| DCHECK(dfs_stack.empty()); |
| |
| // It's possible that the number of nodes in the bookmark model is not the |
| // same as number of nodes in the sync model. This can happen when the sync |
| // model doesn't get a chance to persist its changes, for example when |
| // Chrome does not shut down gracefully. In such cases we can't trust the |
| // loaded associations. |
| return sync_node_count == id_index.count(); |
| } |
| |
| } // namespace browser_sync |