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