Index: chrome/browser/sync/glue/bookmark_model_associator.cc |
diff --git a/chrome/browser/sync/glue/bookmark_model_associator.cc b/chrome/browser/sync/glue/bookmark_model_associator.cc |
index 29325970137f40f063c614c2d672c5b205161a10..e8f561967e92a994dd8355ca5f75bb9001516398 100644 |
--- a/chrome/browser/sync/glue/bookmark_model_associator.cc |
+++ b/chrome/browser/sync/glue/bookmark_model_associator.cc |
@@ -76,11 +76,21 @@ class BookmarkNodeFinder { |
explicit BookmarkNodeFinder(const BookmarkNode* parent_node); |
// Finds the bookmark node that matches the given url, title and folder |
- // attribute. Returns the matching node if one exists; NULL otherwise. If a |
- // matching node is found, it's removed for further matches. |
+ // attribute. Returns the matching node if one exists; NULL otherwise. |
+ // If there are multiple matches then a node with ID matching |preferred_id| |
+ // is returned; otherwise the first matching node is returned. |
+ // If a matching node is found, it's removed for further matches. |
const BookmarkNode* FindBookmarkNode(const GURL& url, |
const std::string& title, |
- bool is_folder); |
+ bool is_folder, |
+ int64 preferred_id); |
+ |
+ // Returns true if |bookmark_node| matches the specified |url|, |
+ // |title|, and |is_folder| flags. |
+ static bool NodeMatches(const BookmarkNode* bookmark_node, |
+ const GURL& url, |
+ const std::string& title, |
+ bool is_folder); |
private: |
// Maps bookmark node titles to instances, duplicates allowed. |
@@ -93,8 +103,8 @@ class BookmarkNodeFinder { |
// Converts and truncates bookmark titles in the form sync does internally |
// to avoid mismatches due to sync munging titles. |
- void ConvertTitleToSyncInternalFormat( |
- const std::string& input, std::string* output); |
+ static void ConvertTitleToSyncInternalFormat(const std::string& input, |
+ std::string* output); |
const BookmarkNode* parent_node_; |
BookmarkNodeMap child_nodes_; |
@@ -132,28 +142,63 @@ BookmarkNodeFinder::BookmarkNodeFinder(const BookmarkNode* parent_node) |
} |
const BookmarkNode* BookmarkNodeFinder::FindBookmarkNode( |
- const GURL& url, const std::string& title, bool is_folder) { |
+ const GURL& url, |
+ const std::string& title, |
+ bool is_folder, |
+ int64 preferred_id) { |
+ const BookmarkNode* match = nullptr; |
+ |
// First lookup a range of bookmarks with the same title. |
std::string adjusted_title; |
ConvertTitleToSyncInternalFormat(title, &adjusted_title); |
BookmarkNodeRange range = child_nodes_.equal_range(adjusted_title); |
+ BookmarkNodeMap::iterator match_iter = range.second; |
for (BookmarkNodeMap::iterator iter = range.first; |
iter != range.second; |
++iter) { |
- |
// Then within the range match the node by the folder bit |
// and the url. |
const BookmarkNode* node = iter->second; |
if (is_folder == node->is_folder() && url == node->url()) { |
- // Remove the matched node so we don't match with it again. |
- child_nodes_.erase(iter); |
- return node; |
+ if (node->id() == preferred_id || preferred_id == 0) { |
+ // Preferred match - use this node. |
+ match = node; |
+ match_iter = iter; |
+ break; |
+ } else if (match == nullptr) { |
+ // First match - continue iterating. |
+ match = node; |
+ match_iter = iter; |
+ } |
} |
} |
- return NULL; |
+ if (match_iter != range.second) { |
+ // Remove the matched node so we don't match with it again. |
+ child_nodes_.erase(match_iter); |
+ } |
+ |
+ return match; |
} |
+/* static */ |
+bool BookmarkNodeFinder::NodeMatches(const BookmarkNode* bookmark_node, |
+ const GURL& url, |
+ const std::string& title, |
+ bool is_folder) { |
+ if (url != bookmark_node->url() || is_folder != bookmark_node->is_folder()) { |
+ return false; |
+ } |
+ |
+ // The title passed to this method comes from a sync directory entry. |
+ // The following two lines are needed to make the native bookmark title |
+ // comparable. The same conversion is used in BookmarkNodeFinder constructor. |
+ std::string bookmark_title = base::UTF16ToUTF8(bookmark_node->GetTitle()); |
+ ConvertTitleToSyncInternalFormat(bookmark_title, &bookmark_title); |
+ return title == bookmark_title; |
+} |
+ |
+/* static */ |
void BookmarkNodeFinder::ConvertTitleToSyncInternalFormat( |
const std::string& input, std::string* output) { |
syncer::SyncAPINameToServerName(input, output); |
@@ -346,27 +391,6 @@ bool BookmarkModelAssociator::SyncModelHasUserCreatedNodes(bool* has_nodes) { |
return true; |
} |
-bool BookmarkModelAssociator::NodesMatch( |
- const BookmarkNode* bookmark, |
- const syncer::BaseNode* sync_node) const { |
- std::string truncated_title = base::UTF16ToUTF8(bookmark->GetTitle()); |
- base::TruncateUTF8ToByteSize(truncated_title, |
- kTitleLimitBytes, |
- &truncated_title); |
- if (truncated_title != sync_node->GetTitle()) |
- return false; |
- if (bookmark->is_folder() != sync_node->GetIsFolder()) |
- return false; |
- if (bookmark->is_url()) { |
- if (bookmark->url() != GURL(sync_node->GetBookmarkSpecifics().url())) |
- 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 BookmarkModelAssociator::AssociateTaggedPermanentNode( |
const BookmarkNode* permanent_node, const std::string&tag) { |
// Do nothing if |permanent_node| is already initialized and associated. |
@@ -424,11 +448,10 @@ syncer::SyncError BookmarkModelAssociator::BuildAssociations( |
// * 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. |
+ // The best match algorithm uses folder title or bookmark title/url to |
+ // perform the primary match. If there are multiple match candidates it |
+ // selects the preferred one based on sync node external ID match to the |
+ // bookmark folder ID. |
DCHECK(bookmark_model_->loaded()); |
@@ -492,6 +515,9 @@ syncer::SyncError BookmarkModelAssociator::BuildAssociations( |
bookmark_model_->root_node()->GetTotalNodeCount()); |
// Remove obsolete bookmarks according to sync delete journal. |
+ // TODO (stanisc): crbug.com/456876: rewrite this to avoid a separate |
+ // traversal and instead perform deletes at the end of the loop below where |
+ // the unmatched bookmark nodes are created as sync nodes. |
local_merge_result->set_num_items_deleted( |
ApplyDeletesFromSyncJournal(&trans)); |
@@ -534,11 +560,10 @@ syncer::SyncError BookmarkModelAssociator::BuildAssociations( |
model_type()); |
} |
- const BookmarkNode* child_node = NULL; |
- child_node = node_finder.FindBookmarkNode( |
+ const BookmarkNode* child_node = node_finder.FindBookmarkNode( |
GURL(sync_child_node.GetBookmarkSpecifics().url()), |
- sync_child_node.GetTitle(), |
- sync_child_node.GetIsFolder()); |
+ sync_child_node.GetTitle(), sync_child_node.GetIsFolder(), |
+ sync_child_node.GetExternalId()); |
if (child_node) { |
Associate(child_node, sync_child_id); |
@@ -616,6 +641,14 @@ int64 BookmarkModelAssociator::ApplyDeletesFromSyncJournal( |
return 0; |
size_t num_journals_unmatched = bk_delete_journals.size(); |
+ // Make a set of all external IDs in the delete journal, |
+ // ignore entries with unset external IDs. |
+ std::set<int64> journaled_external_ids; |
+ for (size_t i = 0; i < num_journals_unmatched; i++) { |
+ if (bk_delete_journals[i].external_id != 0) |
+ journaled_external_ids.insert(bk_delete_journals[i].external_id); |
+ } |
+ |
// Check bookmark model from top to bottom. |
std::stack<const BookmarkNode*> dfs_stack; |
dfs_stack.push(bookmark_model_->bookmark_bar_node()); |
@@ -630,40 +663,54 @@ int64 BookmarkModelAssociator::ApplyDeletesFromSyncJournal( |
// bookmarks are removed in first pass, recheck the folders in reverse order |
// to remove empty ones. |
FolderInfoList folders_matched; |
- while (!dfs_stack.empty()) { |
+ while (!dfs_stack.empty() && num_journals_unmatched > 0) { |
const BookmarkNode* parent = dfs_stack.top(); |
dfs_stack.pop(); |
+ DCHECK(parent->is_folder()); |
+ |
+ // Enumerate folder children in reverse order to make it easier to remove |
+ // bookmarks matching entries in the delete journal. |
+ for (int child_index = parent->child_count() - 1; |
+ child_index >= 0 && num_journals_unmatched > 0; --child_index) { |
+ const BookmarkNode* child = parent->GetChild(child_index); |
+ if (child->is_folder()) |
+ dfs_stack.push(child); |
+ |
+ if (journaled_external_ids.find(child->id()) == |
+ journaled_external_ids.end()) { |
+ // Skip bookmark node which id is not in the set of external IDs. |
+ continue; |
+ } |
- BookmarkNodeFinder finder(parent); |
- // Iterate through journals from back to front. Remove matched journal by |
- // moving an unmatched journal at the tail to its position so that we can |
- // read unmatched journals off the head in next loop. |
- for (int i = num_journals_unmatched - 1; i >= 0; --i) { |
- const BookmarkNode* child = finder.FindBookmarkNode( |
- GURL(bk_delete_journals[i].specifics.bookmark().url()), |
- bk_delete_journals[i].specifics.bookmark().title(), |
- bk_delete_journals[i].is_folder); |
- if (child) { |
- if (child->is_folder()) { |
- // Remember matched folder without removing and delete only empty |
- // ones later. |
- folders_matched.push_back(FolderInfo(child, parent, |
- bk_delete_journals[i].id)); |
- } else { |
- bookmark_model_->Remove(parent, parent->GetIndexOf(child)); |
- ++num_bookmark_deleted; |
+ // Iterate through the journal entries from back to front. Remove matched |
+ // journal by moving an unmatched entry at the tail to the matched |
+ // position so that we can read unmatched entries off the head in next |
+ // loop. |
+ for (int journal_index = num_journals_unmatched - 1; journal_index >= 0; |
+ --journal_index) { |
+ const syncer::BookmarkDeleteJournal& delete_entry = |
+ bk_delete_journals[journal_index]; |
+ if (child->id() == delete_entry.external_id && |
+ BookmarkNodeFinder::NodeMatches( |
+ child, GURL(delete_entry.specifics.bookmark().url()), |
+ delete_entry.specifics.bookmark().title(), |
+ delete_entry.is_folder)) { |
+ if (child->is_folder()) { |
+ // Remember matched folder without removing and delete only empty |
+ // ones later. |
+ folders_matched.push_back( |
+ FolderInfo(child, parent, delete_entry.id)); |
+ } else { |
+ bookmark_model_->Remove(parent, child_index); |
+ ++num_bookmark_deleted; |
+ } |
+ // Move unmatched journal here and decrement counter. |
+ bk_delete_journals[journal_index] = |
+ bk_delete_journals[--num_journals_unmatched]; |
+ break; |
} |
- // Move unmatched journal here and decrement counter. |
- bk_delete_journals[i] = bk_delete_journals[--num_journals_unmatched]; |
} |
} |
- if (num_journals_unmatched == 0) |
- break; |
- |
- for (int i = 0; i < parent->child_count(); ++i) { |
- if (parent->GetChild(i)->is_folder()) |
- dfs_stack.push(parent->GetChild(i)); |
- } |
} |
// Ids of sync nodes not found in bookmark model, meaning the deletions are |