OLD | NEW |
| (Empty) |
1 // Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #ifdef CHROME_PERSONALIZATION | |
6 | |
7 #include "chrome/browser/sync/glue/model_associator.h" | |
8 | |
9 #include <stack> | |
10 | |
11 #include "base/message_loop.h" | |
12 #include "base/task.h" | |
13 #include "chrome/browser/bookmarks/bookmark_model.h" | |
14 #include "chrome/browser/sync/engine/syncapi.h" | |
15 #include "chrome/browser/sync/profile_sync_service.h" | |
16 | |
17 namespace browser_sync { | |
18 | |
19 // The sync protocol identifies top-level entities by means of well-known tags, | |
20 // which should not be confused with titles. Each tag corresponds to a | |
21 // singleton instance of a particular top-level node in a user's share; the | |
22 // tags are consistent across users. The tags allow us to locate the specific | |
23 // folders whose contents we care about synchronizing, without having to do a | |
24 // lookup by name or path. The tags should not be made user-visible. | |
25 // For example, the tag "bookmark_bar" represents the permanent node for | |
26 // bookmarks bar in Chrome. The tag "other_bookmarks" represents the permanent | |
27 // folder Other Bookmarks in Chrome. | |
28 // | |
29 // It is the responsibility of something upstream (at time of writing, | |
30 // the sync server) to create these tagged nodes when initializing sync | |
31 // for the first time for a user. Thus, once the backend finishes | |
32 // initializing, the ProfileSyncService can rely on the presence of tagged | |
33 // nodes. | |
34 // | |
35 // TODO(ncarter): Pull these tags from an external protocol specification | |
36 // rather than hardcoding them here. | |
37 static const wchar_t* kOtherBookmarksTag = L"other_bookmarks"; | |
38 static const wchar_t* kBookmarkBarTag = L"bookmark_bar"; | |
39 | |
40 // Bookmark comparer for map of bookmark nodes. | |
41 class BookmarkComparer { | |
42 public: | |
43 // Compares the two given nodes and returns whether node1 should appear | |
44 // before node2 in strict weak ordering. | |
45 bool operator()(const BookmarkNode* node1, | |
46 const BookmarkNode* node2) const { | |
47 DCHECK(node1); | |
48 DCHECK(node2); | |
49 | |
50 // Keep folder nodes before non-folder nodes. | |
51 if (node1->is_folder() != node2->is_folder()) | |
52 return node1->is_folder(); | |
53 | |
54 int result = node1->GetTitle().compare(node2->GetTitle()); | |
55 if (result != 0) | |
56 return result < 0; | |
57 | |
58 result = node1->GetURL().spec().compare(node2->GetURL().spec()); | |
59 if (result != 0) | |
60 return result < 0; | |
61 | |
62 return false; | |
63 } | |
64 }; | |
65 | |
66 // Provides the following abstraction: given a parent bookmark node, find best | |
67 // matching child node for many sync nodes. | |
68 class BookmarkNodeFinder { | |
69 public: | |
70 // Creats an instance with the given parent bookmark node. | |
71 explicit BookmarkNodeFinder(const BookmarkNode* parent_node); | |
72 | |
73 // Finds best matching node for the given sync node. | |
74 // Returns the matching node if one exists; NULL otherwise. If a matching | |
75 // node is found, it's removed for further matches. | |
76 const BookmarkNode* FindBookmarkNode(const sync_api::BaseNode& sync_node); | |
77 | |
78 private: | |
79 typedef std::set<const BookmarkNode*, BookmarkComparer> BookmarkNodesSet; | |
80 | |
81 const BookmarkNode* parent_node_; | |
82 BookmarkNodesSet child_nodes_; | |
83 | |
84 DISALLOW_COPY_AND_ASSIGN(BookmarkNodeFinder); | |
85 }; | |
86 | |
87 BookmarkNodeFinder::BookmarkNodeFinder(const BookmarkNode* parent_node) | |
88 : parent_node_(parent_node) { | |
89 for (int i = 0; i < parent_node_->GetChildCount(); ++i) | |
90 child_nodes_.insert(parent_node_->GetChild(i)); | |
91 } | |
92 | |
93 const BookmarkNode* BookmarkNodeFinder::FindBookmarkNode( | |
94 const sync_api::BaseNode& sync_node) { | |
95 // Create a bookmark node from the given sync node. | |
96 BookmarkNode temp_node(GURL(sync_node.GetURL())); | |
97 temp_node.SetTitle(UTF16ToWide(sync_node.GetTitle())); | |
98 if (sync_node.GetIsFolder()) | |
99 temp_node.SetType(BookmarkNode::FOLDER); | |
100 else | |
101 temp_node.SetType(BookmarkNode::URL); | |
102 | |
103 const BookmarkNode* result = NULL; | |
104 BookmarkNodesSet::iterator iter = child_nodes_.find(&temp_node); | |
105 if (iter != child_nodes_.end()) { | |
106 result = *iter; | |
107 // Remove the matched node so we don't match with it again. | |
108 child_nodes_.erase(iter); | |
109 } | |
110 | |
111 return result; | |
112 } | |
113 | |
114 ModelAssociator::ModelAssociator(ProfileSyncService* sync_service) | |
115 : sync_service_(sync_service), | |
116 task_pending_(false) { | |
117 DCHECK(sync_service_); | |
118 } | |
119 | |
120 void ModelAssociator::ClearAll() { | |
121 id_map_.clear(); | |
122 id_map_inverse_.clear(); | |
123 dirty_assocations_sync_ids_.clear(); | |
124 } | |
125 | |
126 int64 ModelAssociator::GetSyncIdFromBookmarkId(int64 node_id) const { | |
127 BookmarkIdToSyncIdMap::const_iterator iter = id_map_.find(node_id); | |
128 return iter == id_map_.end() ? sync_api::kInvalidId : iter->second; | |
129 } | |
130 | |
131 bool ModelAssociator::GetBookmarkIdFromSyncId(int64 sync_id, | |
132 int64* node_id) const { | |
133 SyncIdToBookmarkIdMap::const_iterator iter = id_map_inverse_.find(sync_id); | |
134 if (iter == id_map_inverse_.end()) | |
135 return false; | |
136 *node_id = iter->second; | |
137 return true; | |
138 } | |
139 | |
140 bool ModelAssociator::InitSyncNodeFromBookmarkId( | |
141 int64 node_id, | |
142 sync_api::BaseNode* sync_node) { | |
143 DCHECK(sync_node); | |
144 int64 sync_id = GetSyncIdFromBookmarkId(node_id); | |
145 if (sync_id == sync_api::kInvalidId) | |
146 return false; | |
147 if (!sync_node->InitByIdLookup(sync_id)) | |
148 return false; | |
149 DCHECK(sync_node->GetId() == sync_id); | |
150 return true; | |
151 } | |
152 | |
153 const BookmarkNode* ModelAssociator::GetBookmarkNodeFromSyncId(int64 sync_id) { | |
154 int64 node_id; | |
155 if (!GetBookmarkIdFromSyncId(sync_id, &node_id)) | |
156 return false; | |
157 BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); | |
158 return model->GetNodeByID(node_id); | |
159 } | |
160 | |
161 void ModelAssociator::AssociateIds(int64 node_id, int64 sync_id) { | |
162 DCHECK_NE(sync_id, sync_api::kInvalidId); | |
163 DCHECK(id_map_.find(node_id) == id_map_.end()); | |
164 DCHECK(id_map_inverse_.find(sync_id) == id_map_inverse_.end()); | |
165 id_map_[node_id] = sync_id; | |
166 id_map_inverse_[sync_id] = node_id; | |
167 dirty_assocations_sync_ids_.insert(sync_id); | |
168 PostPersistAssociationsTask(); | |
169 } | |
170 | |
171 void ModelAssociator::DisassociateIds(int64 sync_id) { | |
172 SyncIdToBookmarkIdMap::iterator iter = id_map_inverse_.find(sync_id); | |
173 if (iter == id_map_inverse_.end()) | |
174 return; | |
175 id_map_.erase(iter->second); | |
176 id_map_inverse_.erase(iter); | |
177 dirty_assocations_sync_ids_.erase(sync_id); | |
178 } | |
179 | |
180 bool ModelAssociator::BookmarkModelHasUserCreatedNodes() const { | |
181 BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); | |
182 DCHECK(model->IsLoaded()); | |
183 return model->GetBookmarkBarNode()->GetChildCount() > 0 || | |
184 model->other_node()->GetChildCount() > 0; | |
185 } | |
186 | |
187 bool ModelAssociator::SyncModelHasUserCreatedNodes() { | |
188 int64 bookmark_bar_sync_id; | |
189 if (!GetSyncIdForTaggedNode(WideToUTF16(kBookmarkBarTag), | |
190 &bookmark_bar_sync_id)) { | |
191 NOTREACHED(); | |
192 return false; | |
193 } | |
194 int64 other_bookmarks_sync_id; | |
195 if (!GetSyncIdForTaggedNode(WideToUTF16(kOtherBookmarksTag), | |
196 &other_bookmarks_sync_id)) { | |
197 NOTREACHED(); | |
198 return false; | |
199 } | |
200 | |
201 sync_api::ReadTransaction trans( | |
202 sync_service_->backend()->GetUserShareHandle()); | |
203 | |
204 sync_api::ReadNode bookmark_bar_node(&trans); | |
205 if (!bookmark_bar_node.InitByIdLookup(bookmark_bar_sync_id)) { | |
206 NOTREACHED(); | |
207 return false; | |
208 } | |
209 | |
210 sync_api::ReadNode other_bookmarks_node(&trans); | |
211 if (!other_bookmarks_node.InitByIdLookup(other_bookmarks_sync_id)) { | |
212 NOTREACHED(); | |
213 return false; | |
214 } | |
215 | |
216 // Sync model has user created nodes if either one of the permanent nodes | |
217 // has children. | |
218 return bookmark_bar_node.GetFirstChildId() != sync_api::kInvalidId || | |
219 other_bookmarks_node.GetFirstChildId() != sync_api::kInvalidId; | |
220 } | |
221 | |
222 bool ModelAssociator::NodesMatch(const BookmarkNode* bookmark, | |
223 const sync_api::BaseNode* sync_node) const { | |
224 if (bookmark->GetTitle() != UTF16ToWide(sync_node->GetTitle())) | |
225 return false; | |
226 if (bookmark->is_folder() != sync_node->GetIsFolder()) | |
227 return false; | |
228 if (bookmark->is_url()) { | |
229 if (bookmark->GetURL() != GURL(sync_node->GetURL())) | |
230 return false; | |
231 } | |
232 // Don't compare favicons here, because they are not really | |
233 // user-updated and we don't have versioning information -- a site changing | |
234 // its favicon shouldn't result in a bookmark mismatch. | |
235 return true; | |
236 } | |
237 | |
238 bool ModelAssociator::AssociateTaggedPermanentNode( | |
239 const BookmarkNode* permanent_node, | |
240 const string16 &tag) { | |
241 // Do nothing if |permanent_node| is already initialized and associated. | |
242 int64 sync_id = GetSyncIdFromBookmarkId(permanent_node->id()); | |
243 if (sync_id != sync_api::kInvalidId) | |
244 return true; | |
245 if (!GetSyncIdForTaggedNode(tag, &sync_id)) | |
246 return false; | |
247 | |
248 AssociateIds(permanent_node->id(), sync_id); | |
249 return true; | |
250 } | |
251 | |
252 bool ModelAssociator::GetSyncIdForTaggedNode(const string16& tag, | |
253 int64* sync_id) { | |
254 sync_api::ReadTransaction trans( | |
255 sync_service_->backend()->GetUserShareHandle()); | |
256 sync_api::ReadNode sync_node(&trans); | |
257 if (!sync_node.InitByTagLookup(tag.c_str())) | |
258 return false; | |
259 *sync_id = sync_node.GetId(); | |
260 return true; | |
261 } | |
262 | |
263 bool ModelAssociator::AssociateModels() { | |
264 // Try to load model associations from persisted associations first. If that | |
265 // succeeds, we don't need to run the complex model matching algorithm. | |
266 if (LoadAssociations()) | |
267 return true; | |
268 | |
269 ClearAll(); | |
270 | |
271 // We couldn't load model assocations from persisted assocations. So build | |
272 // them. | |
273 return BuildAssocations(); | |
274 } | |
275 | |
276 bool ModelAssociator::BuildAssocations() { | |
277 // Algorithm description: | |
278 // Match up the roots and recursively do the following: | |
279 // * For each sync node for the current sync parent node, find the best | |
280 // matching bookmark node under the corresponding bookmark parent node. | |
281 // If no matching node is found, create a new bookmark node in the same | |
282 // position as the corresponding sync node. | |
283 // If a matching node is found, update the properties of it from the | |
284 // corresponding sync node. | |
285 // * When all children sync nodes are done, add the extra children bookmark | |
286 // nodes to the sync parent node. | |
287 // | |
288 // This algorithm will do a good job of merging when folder names are a good | |
289 // indicator of the two folders being the same. It will handle reordering and | |
290 // new node addition very well (without creating duplicates). | |
291 // This algorithm will not do well if the folder name has changes but the | |
292 // children under them are all the same. | |
293 | |
294 BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); | |
295 DCHECK(model->IsLoaded()); | |
296 | |
297 // To prime our association, we associate the top-level nodes, Bookmark Bar | |
298 // and Other Bookmarks. | |
299 if (!AssociateTaggedPermanentNode(model->other_node(), | |
300 WideToUTF16(kOtherBookmarksTag))) { | |
301 NOTREACHED() << "Server did not create top-level nodes. Possibly we " | |
302 << "are running against an out-of-date server?"; | |
303 return false; | |
304 } | |
305 if (!AssociateTaggedPermanentNode(model->GetBookmarkBarNode(), | |
306 WideToUTF16(kBookmarkBarTag))) { | |
307 NOTREACHED() << "Server did not create top-level nodes. Possibly we " | |
308 << "are running against an out-of-date server?"; | |
309 return false; | |
310 } | |
311 int64 bookmark_bar_sync_id = GetSyncIdFromBookmarkId( | |
312 model->GetBookmarkBarNode()->id()); | |
313 DCHECK(bookmark_bar_sync_id != sync_api::kInvalidId); | |
314 int64 other_bookmarks_sync_id = GetSyncIdFromBookmarkId( | |
315 model->other_node()->id()); | |
316 DCHECK(other_bookmarks_sync_id!= sync_api::kInvalidId); | |
317 | |
318 std::stack<int64> dfs_stack; | |
319 dfs_stack.push(other_bookmarks_sync_id); | |
320 dfs_stack.push(bookmark_bar_sync_id); | |
321 | |
322 sync_api::WriteTransaction trans( | |
323 sync_service_->backend()->GetUserShareHandle()); | |
324 | |
325 while (!dfs_stack.empty()) { | |
326 int64 sync_parent_id = dfs_stack.top(); | |
327 dfs_stack.pop(); | |
328 | |
329 sync_api::ReadNode sync_parent(&trans); | |
330 if (!sync_parent.InitByIdLookup(sync_parent_id)) { | |
331 NOTREACHED(); | |
332 return false; | |
333 } | |
334 // Only folder nodes are pushed on to the stack. | |
335 DCHECK(sync_parent.GetIsFolder()); | |
336 | |
337 const BookmarkNode* parent_node = GetBookmarkNodeFromSyncId(sync_parent_id); | |
338 DCHECK(parent_node->is_folder()); | |
339 | |
340 BookmarkNodeFinder node_finder(parent_node); | |
341 | |
342 int index = 0; | |
343 int64 sync_child_id = sync_parent.GetFirstChildId(); | |
344 while (sync_child_id != sync_api::kInvalidId) { | |
345 sync_api::WriteNode sync_child_node(&trans); | |
346 if (!sync_child_node.InitByIdLookup(sync_child_id)) { | |
347 NOTREACHED(); | |
348 return false; | |
349 } | |
350 | |
351 const BookmarkNode* child_node = NULL; | |
352 child_node = node_finder.FindBookmarkNode(sync_child_node); | |
353 if (child_node) { | |
354 model->Move(child_node, parent_node, index); | |
355 // Set the favicon for bookmark node from sync node or vice versa. | |
356 if (!sync_service_->SetBookmarkFavicon(&sync_child_node, child_node)) | |
357 sync_service_->SetSyncNodeFavicon(child_node, &sync_child_node); | |
358 } else { | |
359 // Create a new bookmark node for the sync node. | |
360 child_node = sync_service_->CreateBookmarkNode(&sync_child_node, | |
361 parent_node, | |
362 index); | |
363 } | |
364 AssociateIds(child_node->id(), sync_child_id); | |
365 if (sync_child_node.GetIsFolder()) | |
366 dfs_stack.push(sync_child_id); | |
367 | |
368 sync_child_id = sync_child_node.GetSuccessorId(); | |
369 ++index; | |
370 } | |
371 | |
372 // At this point all the children nodes of the parent sync node have | |
373 // corresponding children in the parent bookmark node and they are all in | |
374 // the right positions: from 0 to index - 1. | |
375 // So the children starting from index in the parent bookmark node are the | |
376 // ones that are not present in the parent sync node. So create them. | |
377 for (int i = index; i < parent_node->GetChildCount(); ++i) { | |
378 sync_child_id = sync_service_->CreateSyncNode(parent_node, i, &trans); | |
379 if (parent_node->GetChild(i)->is_folder()) | |
380 dfs_stack.push(sync_child_id); | |
381 } | |
382 } | |
383 return true; | |
384 } | |
385 | |
386 void ModelAssociator::PostPersistAssociationsTask() { | |
387 // No need to post a task if a task is already pending. | |
388 if (task_pending_) | |
389 return; | |
390 task_pending_ = true; | |
391 MessageLoop::current()->PostTask( | |
392 FROM_HERE, | |
393 NewRunnableMethod(this, &ModelAssociator::PersistAssociations)); | |
394 } | |
395 | |
396 void ModelAssociator::PersistAssociations() { | |
397 DCHECK(task_pending_); | |
398 task_pending_ = false; | |
399 | |
400 // If there are no dirty assocations we have nothing to do. We handle this | |
401 // explicity instead of letting the for loop do it to avoid creating a write | |
402 // transaction in this case. | |
403 if (dirty_assocations_sync_ids_.empty()) { | |
404 DCHECK(id_map_.empty()); | |
405 DCHECK(id_map_inverse_.empty()); | |
406 return; | |
407 } | |
408 | |
409 sync_api::WriteTransaction trans( | |
410 sync_service_->backend()->GetUserShareHandle()); | |
411 DirtyAssocationsSyncIds::iterator iter; | |
412 for (iter = dirty_assocations_sync_ids_.begin(); | |
413 iter != dirty_assocations_sync_ids_.end(); | |
414 ++iter) { | |
415 int64 sync_id = *iter; | |
416 sync_api::WriteNode sync_node(&trans); | |
417 if (!sync_node.InitByIdLookup(sync_id)) { | |
418 sync_service_->SetUnrecoverableError(); | |
419 return; | |
420 } | |
421 int64 node_id; | |
422 if (GetBookmarkIdFromSyncId(sync_id, &node_id)) | |
423 sync_node.SetExternalId(node_id); | |
424 else | |
425 NOTREACHED(); | |
426 } | |
427 dirty_assocations_sync_ids_.clear(); | |
428 } | |
429 | |
430 bool ModelAssociator::LoadAssociations() { | |
431 BookmarkModel* model = sync_service_->profile()->GetBookmarkModel(); | |
432 DCHECK(model->IsLoaded()); | |
433 // If the bookmarks changed externally, our previous assocations may not be | |
434 // valid; so return false. | |
435 if (model->file_changed()) | |
436 return false; | |
437 | |
438 // Our persisted assocations should be valid. Try to populate id assocation | |
439 // maps using persisted assocations. | |
440 | |
441 int64 other_bookmarks_id; | |
442 if (!GetSyncIdForTaggedNode(WideToUTF16(kOtherBookmarksTag), | |
443 &other_bookmarks_id)) { | |
444 NOTREACHED(); // We should always be able to find the permanent nodes. | |
445 return false; | |
446 } | |
447 int64 bookmark_bar_id; | |
448 if (!GetSyncIdForTaggedNode(WideToUTF16(kBookmarkBarTag), &bookmark_bar_id)) { | |
449 NOTREACHED(); // We should always be able to find the permanent nodes. | |
450 return false; | |
451 } | |
452 | |
453 std::stack<int64> dfs_stack; | |
454 dfs_stack.push(other_bookmarks_id); | |
455 dfs_stack.push(bookmark_bar_id); | |
456 | |
457 sync_api::ReadTransaction trans( | |
458 sync_service_->backend()->GetUserShareHandle()); | |
459 | |
460 while (!dfs_stack.empty()) { | |
461 int64 parent_id = dfs_stack.top(); | |
462 dfs_stack.pop(); | |
463 sync_api::ReadNode sync_parent(&trans); | |
464 if (!sync_parent.InitByIdLookup(parent_id)) { | |
465 NOTREACHED(); | |
466 return false; | |
467 } | |
468 | |
469 int64 external_id = sync_parent.GetExternalId(); | |
470 if (external_id == 0) | |
471 return false; | |
472 | |
473 const BookmarkNode* node = model->GetNodeByID(external_id); | |
474 if (!node) | |
475 return false; | |
476 | |
477 // Don't try to call NodesMatch on permanent nodes like bookmark bar and | |
478 // other bookmarks. They are not expected to match. | |
479 if (node != model->GetBookmarkBarNode() && | |
480 node != model->other_node() && | |
481 !NodesMatch(node, &sync_parent)) | |
482 return false; | |
483 | |
484 AssociateIds(external_id, sync_parent.GetId()); | |
485 | |
486 // Add all children of the current node to the stack. | |
487 int64 child_id = sync_parent.GetFirstChildId(); | |
488 while (child_id != sync_api::kInvalidId) { | |
489 dfs_stack.push(child_id); | |
490 sync_api::ReadNode child_node(&trans); | |
491 if (!child_node.InitByIdLookup(child_id)) { | |
492 NOTREACHED(); | |
493 return false; | |
494 } | |
495 child_id = child_node.GetSuccessorId(); | |
496 } | |
497 } | |
498 DCHECK(dfs_stack.empty()); | |
499 return true; | |
500 } | |
501 | |
502 } // namespace browser_sync | |
503 | |
504 #endif // CHROME_PERSONALIZATION | |
OLD | NEW |