1// Copyright (c) 2011 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#include "chrome/browser/sync/glue/typed_url_change_processor.h"
6
7#include "base/string_util.h"
8#include "base/utf_string_conversions.h"
9#include "chrome/browser/history/history_backend.h"
10#include "chrome/browser/history/history_notifications.h"
11#include "chrome/browser/profiles/profile.h"
12#include "chrome/browser/sync/glue/typed_url_model_associator.h"
13#include "chrome/browser/sync/profile_sync_service.h"
14#include "chrome/browser/sync/protocol/typed_url_specifics.pb.h"
15#include "content/common/notification_service.h"
16#include "content/common/notification_type.h"
17
18namespace browser_sync {
19
20TypedUrlChangeProcessor::TypedUrlChangeProcessor(
21    TypedUrlModelAssociator* model_associator,
22    history::HistoryBackend* history_backend,
23    UnrecoverableErrorHandler* error_handler)
24    : ChangeProcessor(error_handler),
25      model_associator_(model_associator),
26      history_backend_(history_backend),
27      observing_(false),
28      expected_loop_(MessageLoop::current()) {
29  DCHECK(model_associator);
30  DCHECK(history_backend);
31  DCHECK(error_handler);
32  DCHECK(!BrowserThread::CurrentlyOn(BrowserThread::UI));
33  // When running in unit tests, there is already a NotificationService object.
34  // Since only one can exist at a time per thread, check first.
35  if (!NotificationService::current())
36    notification_service_.reset(new NotificationService);
37  StartObserving();
38}
39
40TypedUrlChangeProcessor::~TypedUrlChangeProcessor() {
41  DCHECK(expected_loop_ == MessageLoop::current());
42}
43
44void TypedUrlChangeProcessor::Observe(NotificationType type,
45                                      const NotificationSource& source,
46                                      const NotificationDetails& details) {
47  DCHECK(expected_loop_ == MessageLoop::current());
48  if (!observing_)
49    return;
50
51  VLOG(1) << "Observed typed_url change.";
52  DCHECK(running());
53  DCHECK(NotificationType::HISTORY_TYPED_URLS_MODIFIED == type ||
54         NotificationType::HISTORY_URLS_DELETED == type ||
55         NotificationType::HISTORY_URL_VISITED == type);
56  if (type == NotificationType::HISTORY_TYPED_URLS_MODIFIED) {
57    HandleURLsModified(Details<history::URLsModifiedDetails>(details).ptr());
58  } else if (type == NotificationType::HISTORY_URLS_DELETED) {
59    HandleURLsDeleted(Details<history::URLsDeletedDetails>(details).ptr());
60  } else if (type == NotificationType::HISTORY_URL_VISITED) {
61    HandleURLsVisited(Details<history::URLVisitedDetails>(details).ptr());
62  }
63}
64
65void TypedUrlChangeProcessor::HandleURLsModified(
66    history::URLsModifiedDetails* details) {
67  // Get all the visits.
68  std::map<history::URLID, history::VisitVector> visit_vectors;
69  for (std::vector<history::URLRow>::iterator url =
70       details->changed_urls.begin(); url != details->changed_urls.end();
71       ++url) {
72    if (!history_backend_->GetVisitsForURL(url->id(),
73                                           &(visit_vectors[url->id()]))) {
74      error_handler()->OnUnrecoverableError(FROM_HERE,
75          "Could not get the url's visits.");
76      return;
77    }
78    DCHECK(!visit_vectors[url->id()].empty());
79  }
80
81  sync_api::WriteTransaction trans(share_handle());
82
83  sync_api::ReadNode typed_url_root(&trans);
84  if (!typed_url_root.InitByTagLookup(kTypedUrlTag)) {
85    error_handler()->OnUnrecoverableError(FROM_HERE,
86        "Server did not create the top-level typed_url node. We "
87         "might be running against an out-of-date server.");
88    return;
89  }
90
91  for (std::vector<history::URLRow>::iterator url =
92       details->changed_urls.begin(); url != details->changed_urls.end();
93       ++url) {
94    std::string tag = url->url().spec();
95
96    history::VisitVector& visits = visit_vectors[url->id()];
97
98    DCHECK(!visits.empty());
99
100    DCHECK(static_cast<size_t>(url->visit_count()) == visits.size());
101    if (static_cast<size_t>(url->visit_count()) != visits.size()) {
102      error_handler()->OnUnrecoverableError(FROM_HERE,
103          "Visit count does not match.");
104      return;
105    }
106
107    sync_api::WriteNode update_node(&trans);
108    if (update_node.InitByClientTagLookup(syncable::TYPED_URLS, tag)) {
109      model_associator_->WriteToSyncNode(*url, visits, &update_node);
110    } else {
111      sync_api::WriteNode create_node(&trans);
112      if (!create_node.InitUniqueByCreation(syncable::TYPED_URLS,
113                                            typed_url_root, tag)) {
114        error_handler()->OnUnrecoverableError(FROM_HERE,
115            "Failed to create typed_url sync node.");
116        return;
117      }
118
119      create_node.SetTitle(UTF8ToWide(tag));
120      model_associator_->WriteToSyncNode(*url, visits, &create_node);
121
122      model_associator_->Associate(&tag, create_node.GetId());
123    }
124  }
125}
126
127void TypedUrlChangeProcessor::HandleURLsDeleted(
128    history::URLsDeletedDetails* details) {
129  sync_api::WriteTransaction trans(share_handle());
130
131  if (details->all_history) {
132    if (!model_associator_->DeleteAllNodes(&trans)) {
133      error_handler()->OnUnrecoverableError(FROM_HERE, std::string());
134      return;
135    }
136  } else {
137    for (std::set<GURL>::iterator url = details->urls.begin();
138         url != details->urls.end(); ++url) {
139      sync_api::WriteNode sync_node(&trans);
140      int64 sync_id =
141      model_associator_->GetSyncIdFromChromeId(url->spec());
142      if (sync_api::kInvalidId != sync_id) {
143        if (!sync_node.InitByIdLookup(sync_id)) {
144          error_handler()->OnUnrecoverableError(FROM_HERE,
145              "Typed url node lookup failed.");
146          return;
147        }
148        model_associator_->Disassociate(sync_node.GetId());
149        sync_node.Remove();
150      }
151    }
152  }
153}
154
155void TypedUrlChangeProcessor::HandleURLsVisited(
156    history::URLVisitedDetails* details) {
157  if (!details->row.typed_count()) {
158    // We only care about typed urls.
159    return;
160  }
161  history::VisitVector visits;
162  if (!history_backend_->GetVisitsForURL(details->row.id(), &visits) ||
163      visits.empty()) {
164    error_handler()->OnUnrecoverableError(FROM_HERE,
165        "Could not get the url's visits.");
166    return;
167  }
168
169  DCHECK(static_cast<size_t>(details->row.visit_count()) == visits.size());
170
171  sync_api::WriteTransaction trans(share_handle());
172  std::string tag = details->row.url().spec();
173  sync_api::WriteNode update_node(&trans);
174  if (!update_node.InitByClientTagLookup(syncable::TYPED_URLS, tag)) {
175    // If we don't know about it yet, it will be added later.
176    return;
177  }
178  sync_pb::TypedUrlSpecifics typed_url(update_node.GetTypedUrlSpecifics());
179  typed_url.add_visit(visits.back().visit_time.ToInternalValue());
180  update_node.SetTypedUrlSpecifics(typed_url);
181}
182
183void TypedUrlChangeProcessor::ApplyChangesFromSyncModel(
184    const sync_api::BaseTransaction* trans,
185    const sync_api::SyncManager::ChangeRecord* changes,
186    int change_count) {
187  DCHECK(expected_loop_ == MessageLoop::current());
188  if (!running())
189    return;
190  StopObserving();
191
192  sync_api::ReadNode typed_url_root(trans);
193  if (!typed_url_root.InitByTagLookup(kTypedUrlTag)) {
194    error_handler()->OnUnrecoverableError(FROM_HERE,
195        "TypedUrl root node lookup failed.");
196    return;
197  }
198
199  TypedUrlModelAssociator::TypedUrlTitleVector titles;
200  TypedUrlModelAssociator::TypedUrlVector new_urls;
201  TypedUrlModelAssociator::TypedUrlVisitVector new_visits;
202  history::VisitVector deleted_visits;
203  TypedUrlModelAssociator::TypedUrlUpdateVector updated_urls;
204
205  for (int i = 0; i < change_count; ++i) {
206    if (sync_api::SyncManager::ChangeRecord::ACTION_DELETE ==
207        changes[i].action) {
208      DCHECK(changes[i].specifics.HasExtension(sync_pb::typed_url)) <<
209          "Typed URL delete change does not have necessary specifics.";
210      GURL url(changes[i].specifics.GetExtension(sync_pb::typed_url).url());
211      history_backend_->DeleteURL(url);
212      continue;
213    }
214
215    sync_api::ReadNode sync_node(trans);
216    if (!sync_node.InitByIdLookup(changes[i].id)) {
217      error_handler()->OnUnrecoverableError(FROM_HERE,
218          "TypedUrl node lookup failed.");
219      return;
220    }
221
222    // Check that the changed node is a child of the typed_urls folder.
223    DCHECK(typed_url_root.GetId() == sync_node.GetParentId());
224    DCHECK(syncable::TYPED_URLS == sync_node.GetModelType());
225
226    const sync_pb::TypedUrlSpecifics& typed_url(
227        sync_node.GetTypedUrlSpecifics());
228    GURL url(typed_url.url());
229
230    if (sync_api::SyncManager::ChangeRecord::ACTION_ADD == changes[i].action) {
231      DCHECK(typed_url.visit_size());
232      if (!typed_url.visit_size()) {
233        continue;
234      }
235
236      history::URLRow new_url(url);
237      new_url.set_title(UTF8ToUTF16(typed_url.title()));
238
239      // When we add a new url, the last visit is always added, thus we set
240      // the initial visit count to one.  This value will be automatically
241      // incremented as visits are added.
242      new_url.set_visit_count(1);
243      new_url.set_typed_count(typed_url.typed_count());
244      new_url.set_hidden(typed_url.hidden());
245
246      new_url.set_last_visit(base::Time::FromInternalValue(
247          typed_url.visit(typed_url.visit_size() - 1)));
248
249      new_urls.push_back(new_url);
250
251      // The latest visit gets added automatically, so skip it.
252      std::vector<base::Time> added_visits;
253      for (int c = 0; c < typed_url.visit_size() - 1; ++c) {
254        DCHECK(typed_url.visit(c) < typed_url.visit(c + 1));
255        added_visits.push_back(
256            base::Time::FromInternalValue(typed_url.visit(c)));
257      }
258
259      new_visits.push_back(
260          std::pair<GURL, std::vector<base::Time> >(url, added_visits));
261    } else {
262      history::URLRow old_url;
263      if (!history_backend_->GetURL(url, &old_url)) {
264        error_handler()->OnUnrecoverableError(FROM_HERE,
265            "TypedUrl db lookup failed.");
266        return;
267      }
268
269      history::VisitVector visits;
270      if (!history_backend_->GetVisitsForURL(old_url.id(), &visits)) {
271        error_handler()->OnUnrecoverableError(FROM_HERE,
272            "Could not get the url's visits.");
273        return;
274      }
275
276      history::URLRow new_url(url);
277      new_url.set_title(UTF8ToUTF16(typed_url.title()));
278      new_url.set_visit_count(old_url.visit_count());
279      new_url.set_typed_count(typed_url.typed_count());
280      new_url.set_last_visit(old_url.last_visit());
281      new_url.set_hidden(typed_url.hidden());
282
283      updated_urls.push_back(
284        std::pair<history::URLID, history::URLRow>(old_url.id(), new_url));
285
286      if (old_url.title().compare(new_url.title()) != 0) {
287        titles.push_back(std::pair<GURL, string16>(new_url.url(),
288                                                   new_url.title()));
289      }
290
291      std::vector<base::Time> added_visits;
292      history::VisitVector removed_visits;
293      TypedUrlModelAssociator::DiffVisits(visits, typed_url,
294                                          &added_visits, &removed_visits);
295      if (added_visits.size()) {
296        new_visits.push_back(
297          std::pair<GURL, std::vector<base::Time> >(url, added_visits));
298      }
299      if (removed_visits.size()) {
300        deleted_visits.insert(deleted_visits.end(), removed_visits.begin(),
301                              removed_visits.end());
302      }
303    }
304  }
305  if (!model_associator_->WriteToHistoryBackend(&titles, &new_urls,
306                                                &updated_urls,
307                                                &new_visits, &deleted_visits)) {
308    error_handler()->OnUnrecoverableError(FROM_HERE,
309        "Could not write to the history backend.");
310    return;
311  }
312
313  StartObserving();
314}
315
316void TypedUrlChangeProcessor::StartImpl(Profile* profile) {
317  DCHECK(expected_loop_ == MessageLoop::current());
318  observing_ = true;
319}
320
321void TypedUrlChangeProcessor::StopImpl() {
322  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
323  observing_ = false;
324}
325
326
327void TypedUrlChangeProcessor::StartObserving() {
328  DCHECK(expected_loop_ == MessageLoop::current());
329  notification_registrar_.Add(this,
330                              NotificationType::HISTORY_TYPED_URLS_MODIFIED,
331                              NotificationService::AllSources());
332  notification_registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED,
333                              NotificationService::AllSources());
334  notification_registrar_.Add(this, NotificationType::HISTORY_URL_VISITED,
335                              NotificationService::AllSources());
336}
337
338void TypedUrlChangeProcessor::StopObserving() {
339  DCHECK(expected_loop_ == MessageLoop::current());
340  notification_registrar_.Remove(this,
341                                 NotificationType::HISTORY_TYPED_URLS_MODIFIED,
342                                 NotificationService::AllSources());
343  notification_registrar_.Remove(this,
344                                 NotificationType::HISTORY_URLS_DELETED,
345                                 NotificationService::AllSources());
346  notification_registrar_.Remove(this,
347                                 NotificationType::HISTORY_URL_VISITED,
348                                 NotificationService::AllSources());
349}
350
351}  // namespace browser_sync
352