Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 1 | // Copyright 2018 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 | #ifndef COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_MODEL_MERGER_H_ |
| 6 | #define COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_MODEL_MERGER_H_ |
| 7 | |
Rushan Suleymanov | c414e15 | 2020-05-19 21:26:54 | [diff] [blame] | 8 | #include <list> |
Raphael Kubo da Costa | cdf3e81 | 2019-12-16 11:39:11 | [diff] [blame] | 9 | #include <memory> |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 10 | #include <string> |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 11 | #include <unordered_map> |
| 12 | #include <vector> |
| 13 | |
Daniel Hosseinian | 6de24fa | 2020-11-17 19:19:53 | [diff] [blame] | 14 | #include "base/guid.h" |
Keishi Hattori | 0e45c02 | 2021-11-27 09:25:52 | [diff] [blame] | 15 | #include "base/memory/raw_ptr.h" |
Rushan Suleymanov | 84c5295c | 2020-04-10 08:01:47 | [diff] [blame] | 16 | #include "components/sync/base/unique_position.h" |
Victor Hugo Vianna Silva | 722d50f | 2020-10-13 16:58:12 | [diff] [blame] | 17 | #include "components/sync/engine/commit_and_get_updates_types.h" |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 18 | |
| 19 | namespace bookmarks { |
| 20 | class BookmarkModel; |
| 21 | class BookmarkNode; |
| 22 | } // namespace bookmarks |
| 23 | |
Mohamed Amir Yosef | 7266a9a | 2018-08-13 14:38:33 | [diff] [blame] | 24 | namespace favicon { |
| 25 | class FaviconService; |
| 26 | } // namespace favicon |
| 27 | |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 28 | namespace sync_bookmarks { |
| 29 | |
| 30 | class SyncedBookmarkTracker; |
| 31 | |
| 32 | // Responsible for merging local and remote bookmark models when bookmark sync |
| 33 | // is enabled for the first time by the user (i.e. no sync metadata exists |
Zhuoyu Qian | 7d6ba79a | 2022-04-13 15:01:31 | [diff] [blame] | 34 | // locally), so we need a best-effort merge based on similarity. It is used by |
| 35 | // the BookmarkModelTypeProcessor(). |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 36 | class BookmarkModelMerger { |
| 37 | public: |
Mikel Astiz | 3681d6a | 2019-11-14 21:01:23 | [diff] [blame] | 38 | // |bookmark_model|, |favicon_service| and |bookmark_tracker| must not be |
| 39 | // null and must outlive this object. |
| 40 | BookmarkModelMerger(syncer::UpdateResponseDataList updates, |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 41 | bookmarks::BookmarkModel* bookmark_model, |
Mohamed Amir Yosef | 7266a9a | 2018-08-13 14:38:33 | [diff] [blame] | 42 | favicon::FaviconService* favicon_service, |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 43 | SyncedBookmarkTracker* bookmark_tracker); |
| 44 | |
Peter Boström | 09c0182 | 2021-09-20 22:43:27 | [diff] [blame] | 45 | BookmarkModelMerger(const BookmarkModelMerger&) = delete; |
| 46 | BookmarkModelMerger& operator=(const BookmarkModelMerger&) = delete; |
| 47 | |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 48 | ~BookmarkModelMerger(); |
| 49 | |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 50 | // Merges the remote bookmark model represented as the updates received from |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 51 | // the sync server and local bookmark model |bookmark_model|, and updates the |
| 52 | // model and |bookmark_tracker| (all of which are injected in the constructor) |
| 53 | // accordingly. On return, there will be a 1:1 mapping between bookmark nodes |
| 54 | // and metadata entities in the injected tracker. |
| 55 | void Merge(); |
| 56 | |
| 57 | private: |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 58 | // Internal representation of a remote tree, composed of nodes. |
Raphael Kubo da Costa | cdf3e81 | 2019-12-16 11:39:11 | [diff] [blame] | 59 | class RemoteTreeNode final { |
| 60 | private: |
Mikel Astiz | 68c787e | 2021-12-19 08:46:49 | [diff] [blame] | 61 | using UpdatesPerParentGUID = |
| 62 | std::unordered_map<base::GUID, |
| 63 | std::list<syncer::UpdateResponseData>, |
| 64 | base::GUIDHash>; |
Raphael Kubo da Costa | cdf3e81 | 2019-12-16 11:39:11 | [diff] [blame] | 65 | |
| 66 | public: |
| 67 | // Constructs a tree given |update| as root and recursively all descendants |
Mikel Astiz | 68c787e | 2021-12-19 08:46:49 | [diff] [blame] | 68 | // by traversing |*updates_per_parent_guid|. |update| and |
| 69 | // |updates_per_parent_guid| must not be null. All updates |
| 70 | // |*updates_per_parent_guid| must represent valid updates. Updates |
Raphael Kubo da Costa | cdf3e81 | 2019-12-16 11:39:11 | [diff] [blame] | 71 | // corresponding from descendant nodes are moved away from |
Mikel Astiz | 68c787e | 2021-12-19 08:46:49 | [diff] [blame] | 72 | // |*updates_per_parent_guid|. |max_depth| is the max tree depth to sync |
Chris Davis | 96a12c7 | 2020-06-26 17:53:36 | [diff] [blame] | 73 | // after which content is silently ignored. |
Mikel Astiz | 68c787e | 2021-12-19 08:46:49 | [diff] [blame] | 74 | static RemoteTreeNode BuildTree( |
| 75 | syncer::UpdateResponseData update, |
| 76 | size_t max_depth, |
| 77 | UpdatesPerParentGUID* updates_per_parent_guid); |
Raphael Kubo da Costa | cdf3e81 | 2019-12-16 11:39:11 | [diff] [blame] | 78 | |
| 79 | ~RemoteTreeNode(); |
| 80 | |
| 81 | // Allow moves, useful during construction. |
| 82 | RemoteTreeNode(RemoteTreeNode&&); |
| 83 | RemoteTreeNode& operator=(RemoteTreeNode&&); |
| 84 | |
Sergey Talantov | ff7224b | 2020-01-09 10:50:07 | [diff] [blame] | 85 | const syncer::EntityData& entity() const { return update_.entity; } |
| 86 | int64_t response_version() const { return update_.response_version; } |
Raphael Kubo da Costa | cdf3e81 | 2019-12-16 11:39:11 | [diff] [blame] | 87 | |
| 88 | // Direct children nodes, sorted by ascending unique position. These are |
| 89 | // guaranteed to be valid updates (e.g. IsValidBookmarkSpecifics()). |
| 90 | const std::vector<RemoteTreeNode>& children() const { return children_; } |
| 91 | |
| 92 | // Recursively emplaces all GUIDs (this node and descendants) into |
| 93 | // |*guid_to_remote_node_map|, which must not be null. |
| 94 | void EmplaceSelfAndDescendantsByGUID( |
Daniel Hosseinian | 6de24fa | 2020-11-17 19:19:53 | [diff] [blame] | 95 | std::unordered_map<base::GUID, const RemoteTreeNode*, base::GUIDHash>* |
Raphael Kubo da Costa | cdf3e81 | 2019-12-16 11:39:11 | [diff] [blame] | 96 | guid_to_remote_node_map) const; |
| 97 | |
| 98 | private: |
| 99 | static bool UniquePositionLessThan(const RemoteTreeNode& lhs, |
| 100 | const RemoteTreeNode& rhs); |
| 101 | |
| 102 | RemoteTreeNode(); |
| 103 | |
Sergey Talantov | ff7224b | 2020-01-09 10:50:07 | [diff] [blame] | 104 | syncer::UpdateResponseData update_; |
Mikel Astiz | a08d966 | 2021-07-28 17:10:34 | [diff] [blame] | 105 | // Redundant, parsed instance of the unique position in specifics, used |
| 106 | // to sort siblings by their position information. |
| 107 | syncer::UniquePosition unique_position_; |
Raphael Kubo da Costa | cdf3e81 | 2019-12-16 11:39:11 | [diff] [blame] | 108 | std::vector<RemoteTreeNode> children_; |
| 109 | }; |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 110 | |
| 111 | // A forest composed of multiple trees where the root of each tree represents |
| 112 | // a permanent node, keyed by server-defined unique tag of the root. |
| 113 | using RemoteForest = std::unordered_map<std::string, RemoteTreeNode>; |
| 114 | |
Mikel Astiz | afc8770 | 2019-11-25 10:53:45 | [diff] [blame] | 115 | // Represents a pair of bookmarks, one local and one remote, that have been |
| 116 | // matched by GUID. They are guaranteed to have the same type and URL (if |
| 117 | // applicable). |
| 118 | struct GuidMatch { |
| 119 | const bookmarks::BookmarkNode* local_node; |
| 120 | const RemoteTreeNode* remote_node; |
| 121 | }; |
| 122 | |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 123 | // Constructs the remote bookmark tree to be merged. Each entry in the |
| 124 | // returned map is a permanent node, identified (keyed) by the server-defined |
| 125 | // tag. All invalid updates are filtered out, including invalid bookmark |
| 126 | // specifics as well as tombstones, in the unlikely event that the server |
| 127 | // sends tombstones as part of the initial download. |
Mikel Astiz | 0bc9b16 | 2021-11-18 14:52:46 | [diff] [blame] | 128 | // |tracker_for_recording_ignored_updates| must not be null and is exclusively |
| 129 | // used to record which updates where ignored because their parent couldn't be |
| 130 | // determined. |
| 131 | static RemoteForest BuildRemoteForest( |
| 132 | syncer::UpdateResponseDataList updates, |
| 133 | SyncedBookmarkTracker* tracker_for_recording_ignored_updates); |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 134 | |
Mikel Astiz | 4e0afd6 | 2021-05-12 13:50:49 | [diff] [blame] | 135 | // Recursively counts and returns the number of descendants for |node|, |
| 136 | // excluding |node| itself. |
| 137 | static int CountRemoteTreeNodeDescendantsForUma( |
| 138 | const BookmarkModelMerger::RemoteTreeNode& node); |
| 139 | |
Mikel Astiz | afc8770 | 2019-11-25 10:53:45 | [diff] [blame] | 140 | // Computes bookmark pairs that should be matched by GUID. Local bookmark |
| 141 | // GUIDs may be regenerated for the case where they collide with a remote GUID |
| 142 | // that is not compatible (e.g. folder vs non-folder). |
Daniel Hosseinian | 6de24fa | 2020-11-17 19:19:53 | [diff] [blame] | 143 | static std::unordered_map<base::GUID, GuidMatch, base::GUIDHash> |
Mikel Astiz | afc8770 | 2019-11-25 10:53:45 | [diff] [blame] | 144 | FindGuidMatchesOrReassignLocal(const RemoteForest& remote_forest, |
| 145 | bookmarks::BookmarkModel* bookmark_model); |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 146 | |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 147 | // Merges a local and a remote subtrees. The input nodes are two equivalent |
| 148 | // local and remote nodes. This method tries to recursively match their |
| 149 | // children. It updates the |bookmark_tracker_| accordingly. |
| 150 | void MergeSubtree(const bookmarks::BookmarkNode* local_node, |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 151 | const RemoteTreeNode& remote_node); |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 152 | |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 153 | // Updates |local_node| to hold same GUID and semantics as its |remote_node| |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 154 | // match. The input nodes are two equivalent local and remote bookmarks that |
| 155 | // are about to be merged. The output node is the potentially replaced |
| 156 | // |local_node|. |local_node| must not be a BookmarkPermanentNode. |
| 157 | const bookmarks::BookmarkNode* UpdateBookmarkNodeFromSpecificsIncludingGUID( |
| 158 | const bookmarks::BookmarkNode* local_node, |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 159 | const RemoteTreeNode& remote_node); |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 160 | |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 161 | // Creates a local bookmark node for a |remote_node|. The local node is |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 162 | // created under |local_parent| at position |index|. If the remote node has |
| 163 | // children, this method recursively creates them as well. It updates the |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 164 | // |bookmark_tracker_| accordingly. |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 165 | void ProcessRemoteCreation(const RemoteTreeNode& remote_node, |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 166 | const bookmarks::BookmarkNode* local_parent, |
Peter Kasting | 8cf4c23e | 2019-06-06 00:38:30 | [diff] [blame] | 167 | size_t index); |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 168 | |
| 169 | // Creates a server counter-part for the local node at position |index| |
| 170 | // under |parent|. If the local node has children, corresponding server nodes |
| 171 | // are created recursively. It updates the |bookmark_tracker_| accordingly and |
| 172 | // new nodes are marked to be committed. |
Peter Kasting | 8cf4c23e | 2019-06-06 00:38:30 | [diff] [blame] | 173 | void ProcessLocalCreation(const bookmarks::BookmarkNode* parent, |
| 174 | size_t index); |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 175 | |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 176 | // Looks for a local node under |local_parent| that matches |remote_node|, |
| 177 | // starting at index |local_child_start_index|. First attempts to find a match |
| 178 | // by GUID and otherwise attempts to find one by semantics. If no match is |
| 179 | // found, a nullptr is returned. |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 180 | const bookmarks::BookmarkNode* FindMatchingLocalNode( |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 181 | const RemoteTreeNode& remote_node, |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 182 | const bookmarks::BookmarkNode* local_parent, |
| 183 | size_t local_child_start_index) const; |
| 184 | |
| 185 | // If |local_node| has a remote counterpart of the same GUID, returns the |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 186 | // corresponding remote node, otherwise returns a nullptr. |local_node| must |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 187 | // not be null. |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 188 | const RemoteTreeNode* FindMatchingRemoteNodeByGUID( |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 189 | const bookmarks::BookmarkNode* local_node) const; |
| 190 | |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 191 | // If |remote_node| has a local counterpart of the same GUID, returns the |
| 192 | // corresponding local node, otherwise returns a nullptr. |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 193 | const bookmarks::BookmarkNode* FindMatchingLocalNodeByGUID( |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 194 | const RemoteTreeNode& remote_node) const; |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 195 | |
| 196 | // Tries to find a child local node under |local_parent| that matches |
| 197 | // |remote_node| semantically and returns the index of that node, as long as |
| 198 | // this local child cannot be matched by GUID to a different node. Matching is |
| 199 | // decided using NodeSemanticsMatch(). It searches in the children list |
| 200 | // starting from position |search_starting_child_index|. In case of no match |
| 201 | // is found, it returns |kInvalidIndex|. |
| 202 | size_t FindMatchingChildBySemanticsStartingAt( |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 203 | const RemoteTreeNode& remote_node, |
Pauline Leitao | d292868 | 2019-09-21 15:26:46 | [diff] [blame] | 204 | const bookmarks::BookmarkNode* local_parent, |
| 205 | size_t starting_child_index) const; |
| 206 | |
Rushan Suleymanov | 84c5295c | 2020-04-10 08:01:47 | [diff] [blame] | 207 | // Used to generate a unique position for the current locally created |
| 208 | // bookmark. |
| 209 | syncer::UniquePosition GenerateUniquePositionForLocalCreation( |
| 210 | const bookmarks::BookmarkNode* parent, |
| 211 | size_t index, |
| 212 | const std::string& suffix) const; |
| 213 | |
Keishi Hattori | 0e45c02 | 2021-11-27 09:25:52 | [diff] [blame] | 214 | const raw_ptr<bookmarks::BookmarkModel> bookmark_model_; |
| 215 | const raw_ptr<favicon::FaviconService> favicon_service_; |
| 216 | const raw_ptr<SyncedBookmarkTracker> bookmark_tracker_; |
Mikel Astiz | 8f5dad9 | 2019-11-19 19:15:52 | [diff] [blame] | 217 | // Preprocessed remote nodes in the form a forest where each tree's root is a |
| 218 | // permanent node. Computed upon construction via BuildRemoteForest(). |
| 219 | const RemoteForest remote_forest_; |
Daniel Hosseinian | 6de24fa | 2020-11-17 19:19:53 | [diff] [blame] | 220 | std::unordered_map<base::GUID, GuidMatch, base::GUIDHash> guid_to_match_map_; |
Mohamed Amir Yosef | a89b53f6 | 2018-08-02 20:17:06 | [diff] [blame] | 221 | }; |
| 222 | |
| 223 | } // namespace sync_bookmarks |
| 224 | |
| 225 | #endif // COMPONENTS_SYNC_BOOKMARKS_BOOKMARK_MODEL_MERGER_H_ |