summaryrefslogtreecommitdiffstats
path: root/chrome/browser/sync/glue
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/sync/glue')
-rw-r--r--chrome/browser/sync/glue/bookmark_model_worker.cc114
-rw-r--r--chrome/browser/sync/glue/bookmark_model_worker.h134
-rw-r--r--chrome/browser/sync/glue/bookmark_model_worker_unittest.cc224
-rw-r--r--chrome/browser/sync/glue/http_bridge.cc252
-rw-r--r--chrome/browser/sync/glue/http_bridge.h171
-rw-r--r--chrome/browser/sync/glue/http_bridge_unittest.cc167
-rw-r--r--chrome/browser/sync/glue/model_associator.cc504
-rw-r--r--chrome/browser/sync/glue/model_associator.h141
-rw-r--r--chrome/browser/sync/glue/sync_backend_host.cc308
-rw-r--r--chrome/browser/sync/glue/sync_backend_host.h274
10 files changed, 2289 insertions, 0 deletions
diff --git a/chrome/browser/sync/glue/bookmark_model_worker.cc b/chrome/browser/sync/glue/bookmark_model_worker.cc
new file mode 100644
index 0000000..be97167
--- /dev/null
+++ b/chrome/browser/sync/glue/bookmark_model_worker.cc
@@ -0,0 +1,114 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/sync/glue/bookmark_model_worker.h"
+
+#include "base/message_loop.h"
+#include "base/waitable_event.h"
+
+namespace browser_sync {
+
+void BookmarkModelWorker::CallDoWorkFromModelSafeThreadAndWait(
+ ModelSafeWorkerInterface::Visitor* visitor) {
+ // It is possible this gets called when we are in the STOPPING state, because
+ // the UI loop has initiated shutdown but the syncer hasn't got the memo yet.
+ // This is fine, the work will get scheduled and run normally or run by our
+ // code handling this case in Stop().
+ DCHECK_NE(state_, STOPPED);
+ if (state_ == STOPPED)
+ return;
+ if (MessageLoop::current() == bookmark_model_loop_) {
+ DLOG(WARNING) << "CallDoWorkFromModelSafeThreadAndWait called from "
+ << "bookmark_model_loop_. Probably a nested invocation?";
+ visitor->DoWork();
+ return;
+ }
+
+ // Create an unsignaled event to wait on.
+ base::WaitableEvent work_done(false, false);
+ {
+ // We lock only to avoid PostTask'ing a NULL pending_work_ (because it
+ // could get Run() in Stop() and call OnTaskCompleted before we post).
+ // The task is owned by the message loop as per usual.
+ AutoLock lock(pending_work_lock_);
+ DCHECK(!pending_work_);
+ pending_work_ = new CallDoWorkAndSignalTask(visitor, &work_done, this);
+ bookmark_model_loop_->PostTask(FROM_HERE, pending_work_);
+ }
+ syncapi_event_.Signal(); // Notify that the syncapi produced work for us.
+ work_done.Wait();
+}
+
+BookmarkModelWorker::~BookmarkModelWorker() {
+ DCHECK_EQ(state_, STOPPED);
+}
+
+void BookmarkModelWorker::OnSyncerShutdownComplete() {
+ // The SyncerThread has terminated and we are no longer needed by syncapi.
+ // The UI loop initiated shutdown and is (or will be) waiting in Stop().
+ // We could either be WORKING or RUNNING_MANUAL_SHUTDOWN_PUMP, depending
+ // on where we timeslice the UI thread in Stop; but we can't be STOPPED,
+ // because that would imply NotifySyncapiShutdownComplete already signaled.
+ DCHECK_NE(state_, STOPPED);
+
+ syncapi_has_shutdown_ = true;
+ syncapi_event_.Signal();
+}
+
+void BookmarkModelWorker::Stop() {
+ DCHECK_EQ(MessageLoop::current(), bookmark_model_loop_);
+ DCHECK_EQ(state_, WORKING);
+
+ // We're on our own now, the beloved UI MessageLoop is no longer running.
+ // Any tasks scheduled or to be scheduled on the UI MessageLoop will not run.
+ state_ = RUNNING_MANUAL_SHUTDOWN_PUMP;
+
+ // Drain any final task manually until the SyncerThread tells us it has
+ // totally finished. Note we use a 'while' loop and not 'if'. The main subtle
+ // reason for this is that syncapi_event could be signaled the first time we
+ // come through due to an old CallDoWork call, and we need to keep looping
+ // until the SyncerThread either calls it again or tells us it is done. There
+ // should only ever be 0 or 1 tasks Run() here, however.
+ while (!syncapi_has_shutdown_) {
+ {
+ AutoLock lock(pending_work_lock_);
+ if (pending_work_)
+ pending_work_->Run();
+ }
+ syncapi_event_.Wait(); // Signaled either by new task, or SyncerThread
+ // termination.
+ }
+
+ state_ = STOPPED;
+}
+
+void BookmarkModelWorker::CallDoWorkAndSignalTask::Run() {
+ if (!visitor_) {
+ // This can happen during tests or cases where there are more than just the
+ // default BookmarkModelWorker in existence and it gets destroyed before
+ // the main UI loop has terminated. There is no easy way to assert the
+ // loop is running / not running at the moment, so we just provide cancel
+ // semantics here and short-circuit.
+ // TODO(timsteele): Maybe we should have the message loop destruction
+ // observer fire when the loop has ended, just a bit before it
+ // actually gets destroyed.
+ return;
+ }
+ visitor_->DoWork();
+
+ // Sever ties with visitor_ to allow the sanity-checking above that we don't
+ // get run twice.
+ visitor_ = NULL;
+
+ // Notify the BookmarkModelWorker that scheduled us that we have run
+ // successfully.
+ scheduler_->OnTaskCompleted();
+ work_done_->Signal(); // Unblock the syncer thread that scheduled us.
+}
+
+} // namespace browser_sync
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/bookmark_model_worker.h b/chrome/browser/sync/glue/bookmark_model_worker.h
new file mode 100644
index 0000000..4ddb5f8
--- /dev/null
+++ b/chrome/browser/sync/glue/bookmark_model_worker.h
@@ -0,0 +1,134 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_GLUE_BOOKMARK_MODEL_WORKER_H_
+#define CHROME_BROWSER_SYNC_GLUE_BOOKMARK_MODEL_WORKER_H_
+
+#include "base/lock.h"
+#include "base/task.h"
+#include "base/waitable_event.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+
+class MessageLoop;
+
+namespace browser_sync {
+
+// A ModelSafeWorker for bookmarks that accepts work requests from the syncapi
+// that need to be fulfilled from the MessageLoop home to the BookmarkModel
+// (this is typically the "main" UI thread).
+//
+// Lifetime note: Instances of this class will generally be owned by the
+// SyncerThread. When the SyncerThread _object_ is destroyed, the
+// BookmarkModelWorker will be destroyed. The SyncerThread object is destroyed
+// after the actual syncer pthread has exited.
+class BookmarkModelWorker :
+ public sync_api::ModelSafeWorkerInterface {
+ public:
+ explicit BookmarkModelWorker(MessageLoop* bookmark_model_loop)
+ : state_(WORKING),
+ pending_work_(NULL),
+ syncapi_has_shutdown_(false),
+ bookmark_model_loop_(bookmark_model_loop),
+ syncapi_event_(false, false) {
+ }
+ virtual ~BookmarkModelWorker();
+
+ // A simple task to signal a waitable event after calling DoWork on a visitor.
+ class CallDoWorkAndSignalTask : public Task {
+ public:
+ CallDoWorkAndSignalTask(ModelSafeWorkerInterface::Visitor* visitor,
+ base::WaitableEvent* work_done,
+ BookmarkModelWorker* scheduler)
+ : visitor_(visitor), work_done_(work_done), scheduler_(scheduler) {
+ }
+ virtual ~CallDoWorkAndSignalTask() { }
+
+ // Task implementation.
+ virtual void Run();
+
+ private:
+ // Task data - a visitor that knows how to DoWork, and a waitable event
+ // to signal after the work has been done.
+ ModelSafeWorkerInterface::Visitor* visitor_;
+ base::WaitableEvent* work_done_;
+
+ // The BookmarkModelWorker responsible for scheduling us.
+ BookmarkModelWorker* const scheduler_;
+
+ DISALLOW_COPY_AND_ASSIGN(CallDoWorkAndSignalTask);
+ };
+
+ // Called by the UI thread on shutdown of the sync service. Blocks until
+ // the BookmarkModelWorker has safely met termination conditions, namely that
+ // no task scheduled by CallDoWorkFromModelSafeThreadAndWait remains un-
+ // processed and that syncapi will not schedule any further work for us to do.
+ void Stop();
+
+ // ModelSafeWorkerInterface implementation. Called on syncapi SyncerThread.
+ virtual void CallDoWorkFromModelSafeThreadAndWait(
+ ModelSafeWorkerInterface::Visitor* visitor);
+
+ // Upon receiving this idempotent call, the ModelSafeWorkerInterface can
+ // assume no work will ever be scheduled again from now on. If it has any work
+ // that it has not yet completed, it must make sure to run it as soon as
+ // possible as the Syncer is trying to shut down. Called from the CoreThread.
+ void OnSyncerShutdownComplete();
+
+ // Callback from |pending_work_| to notify us that it has been run.
+ // Called on |bookmark_model_loop_|.
+ void OnTaskCompleted() { pending_work_ = NULL; }
+
+ private:
+ // The life-cycle of a BookmarkModelWorker in three states.
+ enum State {
+ // We hit the ground running in this state and remain until
+ // the UI loop calls Stop().
+ WORKING,
+ // Stop() sequence has been initiated, but we have not received word that
+ // the SyncerThread has terminated and doesn't need us anymore. Since the
+ // UI MessageLoop is not running at this point, we manually process any
+ // last pending_task_ that the Syncer throws at us, effectively dedicating
+ // the UI thread to terminating the Syncer.
+ RUNNING_MANUAL_SHUTDOWN_PUMP,
+ // We have come to a complete stop, no scheduled work remains, and no work
+ // will be scheduled from now until our destruction.
+ STOPPED,
+ };
+
+ // This is set by the UI thread, but is not explicitly thread safe, so only
+ // read this value from other threads when you know it is absolutely safe (e.g
+ // there is _no_ way we can be in CallDoWork with state_ = STOPPED, so it is
+ // safe to read / compare in this case).
+ State state_;
+
+ // We keep a reference to any task we have scheduled so we can gracefully
+ // force them to run if the syncer is trying to shutdown.
+ Task* pending_work_;
+ Lock pending_work_lock_;
+
+ // Set by the SyncCoreThread when Syncapi shutdown has completed and the
+ // SyncerThread has terminated, so no more work will be scheduled. Read by
+ // the UI thread in Stop().
+ bool syncapi_has_shutdown_;
+
+ // The BookmarkModel's home-sweet-home MessageLoop.
+ MessageLoop* const bookmark_model_loop_;
+
+ // Used as a barrier at shutdown to ensure the SyncerThread terminates before
+ // we allow the UI thread to return from Stop(). This gets signalled whenever
+ // one of two events occur: a new pending_work_ task was scheduled, or the
+ // SyncerThread has terminated. We only care about (1) when we are in Stop(),
+ // because we have to manually Run() the task.
+ base::WaitableEvent syncapi_event_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkModelWorker);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_GLUE_BOOKMARK_MODEL_WORKER_H_
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/bookmark_model_worker_unittest.cc b/chrome/browser/sync/glue/bookmark_model_worker_unittest.cc
new file mode 100644
index 0000000..ad674a8
--- /dev/null
+++ b/chrome/browser/sync/glue/bookmark_model_worker_unittest.cc
@@ -0,0 +1,224 @@
+// Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+#ifdef CHROME_PERSONALIZATION
+
+#include "base/thread.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/glue/bookmark_model_worker.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using browser_sync::BookmarkModelWorker;
+using namespace sync_api;
+
+// Various boilerplate, primarily for the StopWithPendingWork test.
+
+class BookmarkModelWorkerVisitor : public ModelSafeWorkerInterface::Visitor {
+ public:
+ BookmarkModelWorkerVisitor(MessageLoop* faux_ui_loop,
+ base::WaitableEvent* was_run,
+ bool quit_loop)
+ : faux_ui_loop_(faux_ui_loop), quit_loop_when_run_(quit_loop),
+ was_run_(was_run) { }
+ virtual ~BookmarkModelWorkerVisitor() { }
+
+ virtual void DoWork() {
+ EXPECT_EQ(MessageLoop::current(), faux_ui_loop_);
+ was_run_->Signal();
+ if (quit_loop_when_run_)
+ MessageLoop::current()->Quit();
+ }
+
+ private:
+ MessageLoop* faux_ui_loop_;
+ bool quit_loop_when_run_;
+ base::WaitableEvent* was_run_;
+ DISALLOW_COPY_AND_ASSIGN(BookmarkModelWorkerVisitor);
+};
+
+// A faux-syncer that only interacts with its model safe worker.
+class Syncer {
+ public:
+ explicit Syncer(BookmarkModelWorker* worker) : worker_(worker){ }
+ ~Syncer() { }
+
+ void SyncShare(BookmarkModelWorkerVisitor* visitor) {
+ worker_->CallDoWorkFromModelSafeThreadAndWait(visitor);
+ }
+ private:
+ BookmarkModelWorker* worker_;
+ DISALLOW_COPY_AND_ASSIGN(Syncer);
+};
+
+// A task run from the SyncerThread to "sync share", ie tell the Syncer to
+// ask it's ModelSafeWorker to do something.
+class FakeSyncShareTask : public Task {
+ public:
+ FakeSyncShareTask(Syncer* syncer, BookmarkModelWorkerVisitor* visitor)
+ : syncer_(syncer), visitor_(visitor) {
+ }
+ virtual void Run() {
+ syncer_->SyncShare(visitor_);
+ }
+ private:
+ Syncer* syncer_;
+ BookmarkModelWorkerVisitor* visitor_;
+ DISALLOW_COPY_AND_ASSIGN(FakeSyncShareTask);
+};
+
+// A task run from the CoreThread to simulate terminating syncapi.
+class FakeSyncapiShutdownTask : public Task {
+ public:
+ FakeSyncapiShutdownTask(base::Thread* syncer_thread,
+ BookmarkModelWorker* worker,
+ base::WaitableEvent** jobs,
+ size_t job_count)
+ : syncer_thread_(syncer_thread), worker_(worker), jobs_(jobs),
+ job_count_(job_count), all_jobs_done_(false, false) { }
+ virtual void Run() {
+ // In real life, we would try and close a sync directory, which would
+ // result in the syncer calling it's own destructor, which results in
+ // the SyncerThread::HaltSyncer being called, which sets the
+ // syncer in RequestEarlyExit mode and waits until the Syncer finishes
+ // SyncShare to remove the syncer from it's watch. Here we just manually
+ // wait until all outstanding jobs are done to simulate what happens in
+ // SyncerThread::HaltSyncer.
+ all_jobs_done_.WaitMany(jobs_, job_count_);
+
+ // These two calls are made from SyncBackendHost::Core::DoShutdown.
+ syncer_thread_->Stop();
+ worker_->OnSyncerShutdownComplete();
+ }
+ private:
+ base::Thread* syncer_thread_;
+ BookmarkModelWorker* worker_;
+ base::WaitableEvent** jobs_;
+ size_t job_count_;
+ base::WaitableEvent all_jobs_done_;
+ DISALLOW_COPY_AND_ASSIGN(FakeSyncapiShutdownTask);
+};
+
+class BookmarkModelWorkerTest : public testing::Test {
+ public:
+ BookmarkModelWorkerTest() : faux_syncer_thread_("FauxSyncerThread"),
+ faux_core_thread_("FauxCoreThread") { }
+
+ virtual void SetUp() {
+ faux_syncer_thread_.Start();
+ bmw_.reset(new BookmarkModelWorker(&faux_ui_loop_));
+ syncer_.reset(new Syncer(bmw_.get()));
+ }
+
+ Syncer* syncer() { return syncer_.get(); }
+ BookmarkModelWorker* bmw() { return bmw_.get(); }
+ base::Thread* core_thread() { return &faux_core_thread_; }
+ base::Thread* syncer_thread() { return &faux_syncer_thread_; }
+ MessageLoop* ui_loop() { return &faux_ui_loop_; }
+ private:
+ MessageLoop faux_ui_loop_;
+ base::Thread faux_syncer_thread_;
+ base::Thread faux_core_thread_;
+ scoped_ptr<BookmarkModelWorker> bmw_;
+ scoped_ptr<Syncer> syncer_;
+};
+
+TEST_F(BookmarkModelWorkerTest, ScheduledWorkRunsOnUILoop) {
+ base::WaitableEvent v_was_run(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> v(
+ new BookmarkModelWorkerVisitor(ui_loop(), &v_was_run, true));
+
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), v.get()));
+
+ // We are on the UI thread, so run our loop to process the
+ // (hopefully) scheduled task from a SyncShare invocation.
+ MessageLoop::current()->Run();
+
+ bmw()->OnSyncerShutdownComplete();
+ bmw()->Stop();
+ syncer_thread()->Stop();
+}
+
+TEST_F(BookmarkModelWorkerTest, StopWithPendingWork) {
+ // What we want to set up is the following:
+ // ("ui_thread" is the thread we are currently executing on)
+ // 1 - simulate the user shutting down the browser, and the ui thread needing
+ // to terminate the core thread.
+ // 2 - the core thread is where the syncapi is accessed from, and so it needs
+ // to shut down the SyncerThread.
+ // 3 - the syncer is waiting on the BookmarkModelWorker to
+ // perform a task for it.
+ // The BookmarkModelWorker's manual shutdown pump will save the day, as the
+ // UI thread is not actually trying to join() the core thread, it is merely
+ // waiting for the SyncerThread to give it work or to finish. After that, it
+ // will join the core thread which should succeed as the SyncerThread has left
+ // the building. Unfortunately this test as written is not provably decidable,
+ // as it will always halt on success, but it may not on failure (namely if
+ // the task scheduled by the Syncer is _never_ run).
+ core_thread()->Start();
+ base::WaitableEvent v_ran(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> v(new BookmarkModelWorkerVisitor(
+ ui_loop(), &v_ran, false));
+ base::WaitableEvent* jobs[] = { &v_ran };
+
+ // The current message loop is not running, so queue a task to cause
+ // BookmarkModelWorker::Stop() to play a crucial role. See comment below.
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), v.get()));
+
+ // This is what gets the core_thread blocked on the syncer_thread.
+ core_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncapiShutdownTask(syncer_thread(), bmw(), jobs, 1));
+
+ // This is what gets the UI thread blocked until NotifyExitRequested,
+ // which is called when FakeSyncapiShutdownTask runs and deletes the syncer.
+ bmw()->Stop();
+
+ EXPECT_FALSE(syncer_thread()->IsRunning());
+ core_thread()->Stop();
+}
+
+TEST_F(BookmarkModelWorkerTest, HypotheticalManualPumpFlooding) {
+ // This situation should not happen in real life because the Syncer should
+ // never send more than one CallDoWork notification after early_exit_requested
+ // has been set, but our BookmarkModelWorker is built to handle this case
+ // nonetheless. It may be needed in the future, and since we support it and
+ // it is not actually exercised in the wild this test is essential.
+ // It is identical to above except we schedule more than one visitor.
+ core_thread()->Start();
+
+ // Our ammunition.
+ base::WaitableEvent fox1_ran(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> fox1(new BookmarkModelWorkerVisitor(
+ ui_loop(), &fox1_ran, false));
+ base::WaitableEvent fox2_ran(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> fox2(new BookmarkModelWorkerVisitor(
+ ui_loop(), &fox2_ran, false));
+ base::WaitableEvent fox3_ran(false, false);
+ scoped_ptr<BookmarkModelWorkerVisitor> fox3(new BookmarkModelWorkerVisitor(
+ ui_loop(), &fox3_ran, false));
+ base::WaitableEvent* jobs[] = { &fox1_ran, &fox2_ran, &fox3_ran };
+
+ // The current message loop is not running, so queue a task to cause
+ // BookmarkModelWorker::Stop() to play a crucial role. See comment below.
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), fox1.get()));
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), fox2.get()));
+
+ // This is what gets the core_thread blocked on the syncer_thread.
+ core_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncapiShutdownTask(syncer_thread(), bmw(), jobs, 3));
+ syncer_thread()->message_loop()->PostTask(FROM_HERE,
+ new FakeSyncShareTask(syncer(), fox3.get()));
+
+ // This is what gets the UI thread blocked until NotifyExitRequested,
+ // which is called when FakeSyncapiShutdownTask runs and deletes the syncer.
+ bmw()->Stop();
+
+ // Was the thread killed?
+ EXPECT_FALSE(syncer_thread()->IsRunning());
+ core_thread()->Stop();
+}
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/http_bridge.cc b/chrome/browser/sync/glue/http_bridge.cc
new file mode 100644
index 0000000..1a01c87
--- /dev/null
+++ b/chrome/browser/sync/glue/http_bridge.cc
@@ -0,0 +1,252 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/sync/glue/http_bridge.h"
+
+#include "base/message_loop.h"
+#include "base/string_util.h"
+#include "chrome/browser/chrome_thread.h"
+#include "chrome/browser/profile.h"
+#include "net/base/cookie_monster.h"
+#include "net/base/load_flags.h"
+#include "net/http/http_network_layer.h"
+#include "net/proxy/proxy_service.h"
+#include "net/url_request/url_request_status.h"
+#include "webkit/glue/webkit_glue.h"
+
+namespace browser_sync {
+
+HttpBridge::RequestContext* HttpBridgeFactory::GetRequestContext() {
+ if (!request_context_) {
+ request_context_ =
+ new HttpBridge::RequestContext(Profile::GetDefaultRequestContext());
+ request_context_->AddRef();
+ }
+ return request_context_;
+}
+
+HttpBridgeFactory::~HttpBridgeFactory() {
+ if (request_context_) {
+ // Clean up request context on IO thread.
+ ChromeThread::GetMessageLoop(ChromeThread::IO)->ReleaseSoon(FROM_HERE,
+ request_context_);
+ request_context_ = NULL;
+ }
+}
+
+sync_api::HttpPostProviderInterface* HttpBridgeFactory::Create() {
+ // TODO(timsteele): We want the active profile request context.
+ HttpBridge* http = new HttpBridge(GetRequestContext(),
+ ChromeThread::GetMessageLoop(ChromeThread::IO));
+ http->AddRef();
+ return http;
+}
+
+void HttpBridgeFactory::Destroy(sync_api::HttpPostProviderInterface* http) {
+ static_cast<HttpBridge*>(http)->Release();
+}
+
+HttpBridge::RequestContext::RequestContext(
+ const URLRequestContext* baseline_context) {
+
+ // Create empty, in-memory cookie store.
+ cookie_store_ = new net::CookieMonster();
+
+ // We don't use a cache for bridged loads, but we do want to share proxy info.
+ host_resolver_ = baseline_context->host_resolver();
+ proxy_service_ = baseline_context->proxy_service();
+ http_transaction_factory_ =
+ net::HttpNetworkLayer::CreateFactory(host_resolver_, proxy_service_);
+
+ // TODO(timsteele): We don't currently listen for pref changes of these
+ // fields or CookiePolicy; I'm not sure we want to strictly follow the
+ // default settings, since for example if the user chooses to block all
+ // cookies, sync will start failing. Also it seems like accept_lang/charset
+ // should be tied to whatever the sync servers expect (if anything). These
+ // fields should probably just be settable by sync backend; though we should
+ // figure out if we need to give the user explicit control over policies etc.
+ accept_language_ = baseline_context->accept_language();
+ accept_charset_ = baseline_context->accept_charset();
+
+ // We default to the browser's user agent. This can (and should) be overridden
+ // with set_user_agent.
+ user_agent_ = webkit_glue::GetUserAgent(GURL());
+}
+
+HttpBridge::RequestContext::~RequestContext() {
+ delete cookie_store_;
+ delete http_transaction_factory_;
+}
+
+HttpBridge::HttpBridge(HttpBridge::RequestContext* context,
+ MessageLoop* io_loop)
+ : context_for_request_(context),
+ url_poster_(NULL),
+ created_on_loop_(MessageLoop::current()),
+ io_loop_(io_loop),
+ request_completed_(false),
+ request_succeeded_(false),
+ http_response_code_(-1),
+ http_post_completed_(false, false),
+ use_io_loop_for_testing_(false) {
+ context_for_request_->AddRef();
+}
+
+HttpBridge::~HttpBridge() {
+ io_loop_->ReleaseSoon(FROM_HERE, context_for_request_);
+}
+
+void HttpBridge::SetUserAgent(const char* user_agent) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ context_for_request_->set_user_agent(user_agent);
+}
+
+void HttpBridge::SetURL(const char* url, int port) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ DCHECK(url_for_request_.is_empty())
+ << "HttpBridge::SetURL called more than once?!";
+ GURL temp(url);
+ GURL::Replacements replacements;
+ std::string port_str = IntToString(port);
+ replacements.SetPort(port_str.c_str(),
+ url_parse::Component(0, port_str.length()));
+ url_for_request_ = temp.ReplaceComponents(replacements);
+}
+
+void HttpBridge::SetPostPayload(const char* content_type,
+ int content_length,
+ const char* content) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ DCHECK(content_type_.empty()) << "Bridge payload already set.";
+ DCHECK_GE(content_length, 0) << "Content length < 0";
+ content_type_ = content_type;
+ if (!content || (content_length == 0)) {
+ DCHECK_EQ(content_length, 0);
+ request_content_ = " "; // TODO(timsteele): URLFetcher requires non-empty
+ // content for POSTs whereas CURL does not, for now
+ // we hack this to support the sync backend.
+ } else {
+ request_content_.assign(content, content_length);
+ }
+}
+
+void HttpBridge::AddCookieForRequest(const char* cookie) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ DCHECK(url_for_request_.is_valid()) << "Valid URL not set.";
+ if (!url_for_request_.is_valid()) return;
+
+ if (!context_for_request_->cookie_store()->SetCookie(url_for_request_,
+ cookie)) {
+ DLOG(WARNING) << "Cookie " << cookie
+ << " could not be added for url: " << url_for_request_ << ".";
+ }
+}
+
+bool HttpBridge::MakeSynchronousPost(int* os_error_code, int* response_code) {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(!request_completed_);
+ DCHECK(url_for_request_.is_valid()) << "Invalid URL for request";
+ DCHECK(!content_type_.empty()) << "Payload not set";
+ DCHECK(context_for_request_->is_user_agent_set()) << "User agent not set";
+
+ io_loop_->PostTask(FROM_HERE, NewRunnableMethod(this,
+ &HttpBridge::CallMakeAsynchronousPost));
+
+ if (!http_post_completed_.Wait()) // Block until network request completes.
+ NOTREACHED(); // See OnURLFetchComplete.
+
+ DCHECK(request_completed_);
+ *os_error_code = os_error_code_;
+ *response_code = http_response_code_;
+ return request_succeeded_;
+}
+
+void HttpBridge::MakeAsynchronousPost() {
+ DCHECK_EQ(MessageLoop::current(), io_loop_);
+ DCHECK(!request_completed_);
+
+ url_poster_ = new URLFetcher(url_for_request_, URLFetcher::POST, this);
+ url_poster_->set_request_context(context_for_request_);
+ url_poster_->set_upload_data(content_type_, request_content_);
+
+ if (use_io_loop_for_testing_)
+ url_poster_->set_io_loop(io_loop_);
+
+ url_poster_->Start();
+}
+
+int HttpBridge::GetResponseContentLength() const {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(request_completed_);
+ return response_content_.size();
+}
+
+const char* HttpBridge::GetResponseContent() const {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(request_completed_);
+ return response_content_.c_str();
+}
+
+int HttpBridge::GetResponseCookieCount() const {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(request_completed_);
+ return response_cookies_.size();
+}
+
+const char* HttpBridge::GetResponseCookieAt(int cookie_number) const {
+ DCHECK_EQ(MessageLoop::current(), created_on_loop_);
+ DCHECK(request_completed_);
+ bool valid_number = (cookie_number >= 0) &&
+ (static_cast<size_t>(cookie_number) < response_cookies_.size());
+ DCHECK(valid_number);
+ if (!valid_number)
+ return NULL;
+ return response_cookies_[cookie_number].c_str();
+}
+
+void HttpBridge::OnURLFetchComplete(const URLFetcher *source, const GURL &url,
+ const URLRequestStatus &status,
+ int response_code,
+ const ResponseCookies &cookies,
+ const std::string &data) {
+ DCHECK_EQ(MessageLoop::current(), io_loop_);
+
+ request_completed_ = true;
+ request_succeeded_ = (URLRequestStatus::SUCCESS == status.status());
+ http_response_code_ = response_code;
+ os_error_code_ = status.os_error();
+
+ // TODO(timsteele): For now we need this "fixup" to match up with what the
+ // sync backend expects. This seems to be non-standard and shouldn't be done
+ // here in HttpBridge, and it breaks part of the unittest.
+ for (size_t i = 0; i < cookies.size(); ++i) {
+ net::CookieMonster::ParsedCookie parsed_cookie(cookies[i]);
+ std::string cookie = " \t \t \t \t \t";
+ cookie += parsed_cookie.Name() + "\t";
+ cookie += parsed_cookie.Value();
+ response_cookies_.push_back(cookie);
+ }
+
+ response_content_ = data;
+
+ // End of the line for url_poster_. It lives only on the io_loop.
+ // We defer deletion because we're inside a callback from a component of the
+ // URLFetcher, so it seems most natural / "polite" to let the stack unwind.
+ io_loop_->DeleteSoon(FROM_HERE, url_poster_);
+ url_poster_ = NULL;
+
+ // Wake the blocked syncer thread in MakeSynchronousPost.
+ // WARNING: DONT DO ANYTHING AFTER THIS CALL! |this| may be deleted!
+ http_post_completed_.Signal();
+}
+
+} // namespace browser_sync
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/http_bridge.h b/chrome/browser/sync/glue/http_bridge.h
new file mode 100644
index 0000000..1e11e90
--- /dev/null
+++ b/chrome/browser/sync/glue/http_bridge.h
@@ -0,0 +1,171 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_GLUE_HTTP_BRIDGE_H_
+#define CHROME_BROWSER_SYNC_GLUE_HTTP_BRIDGE_H_
+
+#include "base/ref_counted.h"
+#include "base/waitable_event.h"
+#include "chrome/browser/net/url_fetcher.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "googleurl/src/gurl.h"
+#include "net/url_request/url_request_context.h"
+#include "testing/gtest/include/gtest/gtest_prod.h"
+
+class MessageLoop;
+class HttpBridgeTest;
+
+namespace browser_sync {
+
+// A bridge between the syncer and Chromium HTTP layers.
+// Provides a way for the sync backend to use Chromium directly for HTTP
+// requests rather than depending on a third party provider (e.g libcurl).
+// This is a one-time use bridge. Create one for each request you want to make.
+// It is RefCountedThreadSafe because it can PostTask to the io loop, and thus
+// needs to stick around across context switches, etc.
+class HttpBridge : public base::RefCountedThreadSafe<HttpBridge>,
+ public sync_api::HttpPostProviderInterface,
+ public URLFetcher::Delegate {
+ public:
+ // A request context used for HTTP requests bridged from the sync backend.
+ // A bridged RequestContext has a dedicated in-memory cookie store and does
+ // not use a cache. Thus the same type can be used for incognito mode.
+ // TODO(timsteele): We subclass here instead of add a factory method on
+ // ChromeURLRequestContext because:
+ // 1) we want the ability to set_user_agent
+ // 2) avoids ifdefs for now
+ // 3) not sure we want to strictly follow settings for cookie policy,
+ // accept lang/charset, since changing these could break syncing.
+ class RequestContext : public URLRequestContext {
+ public:
+ // |baseline_context| is used to obtain the accept-language,
+ // accept-charsets, and proxy service information for bridged requests.
+ // Typically |baseline_context| should be the URLRequestContext of the
+ // currently active profile.
+ explicit RequestContext(const URLRequestContext* baseline_context);
+ virtual ~RequestContext();
+
+ // Set the user agent for requests using this context. The default is
+ // the browser's UA string.
+ void set_user_agent(const std::string& ua) { user_agent_ = ua; }
+ bool is_user_agent_set() const { return !user_agent_.empty(); }
+
+ virtual const std::string& GetUserAgent(const GURL& url) const {
+ // If the user agent is set explicitly return that, otherwise call the
+ // base class method to return default value.
+ return user_agent_.empty() ?
+ URLRequestContext::GetUserAgent(url) : user_agent_;
+ }
+
+ private:
+ std::string user_agent_;
+
+ DISALLOW_COPY_AND_ASSIGN(RequestContext);
+ };
+
+ HttpBridge(RequestContext* context, MessageLoop* io_loop);
+ virtual ~HttpBridge();
+
+ // sync_api::HttpPostProvider implementation.
+ virtual void SetUserAgent(const char* user_agent);
+ virtual void SetURL(const char* url, int port);
+ virtual void SetPostPayload(const char* content_type, int content_length,
+ const char* content);
+ virtual void AddCookieForRequest(const char* cookie);
+ virtual bool MakeSynchronousPost(int* os_error_code, int* response_code);
+ virtual int GetResponseContentLength() const;
+ virtual const char* GetResponseContent() const;
+ virtual int GetResponseCookieCount() const;
+ virtual const char* GetResponseCookieAt(int cookie_number) const;
+
+ // URLFetcher::Delegate implementation.
+ virtual void OnURLFetchComplete(const URLFetcher* source, const GURL& url,
+ const URLRequestStatus& status,
+ int response_code,
+ const ResponseCookies& cookies,
+ const std::string& data);
+
+ protected:
+ // Protected virtual so the unit test can override to shunt network requests.
+ virtual void MakeAsynchronousPost();
+
+ private:
+ friend class ::HttpBridgeTest;
+
+ // Called on the io_loop_ to issue the network request. The extra level
+ // of indirection is so that the unit test can override this behavior but we
+ // still have a function to statically pass to PostTask.
+ void CallMakeAsynchronousPost() { MakeAsynchronousPost(); }
+
+ // A customized URLRequestContext for bridged requests. See RequestContext
+ // definition for details.
+ RequestContext* context_for_request_;
+
+ // Our hook into the network layer is a URLFetcher. USED ONLY ON THE IO LOOP,
+ // so we can block created_on_loop_ while the fetch is in progress.
+ // NOTE: This is not a scoped_ptr for a reason. It must be deleted on the same
+ // thread that created it, which isn't the same thread |this| gets deleted on.
+ // We must manually delete url_poster_ on the io_loop_.
+ URLFetcher* url_poster_;
+
+ // The message loop of the thread we were created on. This is the thread that
+ // will block on MakeSynchronousPost while the IO thread fetches data from
+ // the network.
+ // This should be the main syncer thread (SyncerThread) which is what blocks
+ // on network IO through curl_easy_perform.
+ MessageLoop* const created_on_loop_;
+
+ // Member variable for the IO loop instead of asking ChromeThread directly,
+ // done this way for testability.
+ MessageLoop* const io_loop_;
+
+ // The URL to POST to.
+ GURL url_for_request_;
+
+ // POST payload information.
+ std::string content_type_;
+ std::string request_content_;
+
+ // Cached response data.
+ bool request_completed_;
+ bool request_succeeded_;
+ int http_response_code_;
+ int os_error_code_;
+ ResponseCookies response_cookies_;
+ std::string response_content_;
+
+ // A waitable event we use to provide blocking semantics to
+ // MakeSynchronousPost. We block created_on_loop_ while the io_loop_ fetches
+ // network request.
+ base::WaitableEvent http_post_completed_;
+
+ // This is here so that the unit test subclass can force our URLFetcher to
+ // use the io_loop_ passed on construction for network requests, rather than
+ // ChromeThread::IO's message loop (which won't exist in testing).
+ bool use_io_loop_for_testing_;
+
+ DISALLOW_COPY_AND_ASSIGN(HttpBridge);
+};
+
+class HttpBridgeFactory
+ : public sync_api::HttpPostProviderFactory {
+ public:
+ HttpBridgeFactory() : request_context_(NULL) { }
+ virtual ~HttpBridgeFactory();
+ virtual sync_api::HttpPostProviderInterface* Create();
+ virtual void Destroy(sync_api::HttpPostProviderInterface* http);
+ private:
+ HttpBridge::RequestContext* GetRequestContext();
+ // We must Release() this from the IO thread.
+ HttpBridge::RequestContext* request_context_;
+ DISALLOW_COPY_AND_ASSIGN(HttpBridgeFactory);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_GLUE_HTTP_BRIDGE_H_
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/http_bridge_unittest.cc b/chrome/browser/sync/glue/http_bridge_unittest.cc
new file mode 100644
index 0000000..4dd9a9b
--- /dev/null
+++ b/chrome/browser/sync/glue/http_bridge_unittest.cc
@@ -0,0 +1,167 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+#ifdef CHROME_PERSONALIZATION
+
+#include "base/thread.h"
+#include "chrome/browser/sync/glue/http_bridge.h"
+#include "net/url_request/url_request_unittest.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using browser_sync::HttpBridge;
+
+namespace {
+// TODO(timsteele): Should use PathService here. See Chromium Issue 3113.
+const char16 kDocRoot[] = L"chrome/test/data";
+}
+
+class HttpBridgeTest : public testing::Test {
+ public:
+ HttpBridgeTest() : io_thread_("HttpBridgeTest IO thread") {
+ }
+
+ virtual void SetUp() {
+ base::Thread::Options options;
+ options.message_loop_type = MessageLoop::TYPE_IO;
+ io_thread_.StartWithOptions(options);
+ }
+
+ virtual void TearDown() {
+ io_thread_.Stop();
+ }
+
+ HttpBridge* BuildBridge() {
+ if (!request_context_) {
+ request_context_ = new HttpBridge::RequestContext(
+ new TestURLRequestContext());
+ }
+ HttpBridge* bridge = new HttpBridge(request_context_,
+ io_thread_.message_loop());
+ bridge->use_io_loop_for_testing_ = true;
+ return bridge;
+ }
+
+ MessageLoop* io_thread_loop() { return io_thread_.message_loop(); }
+ private:
+ // Separate thread for IO used by the HttpBridge.
+ scoped_refptr<HttpBridge::RequestContext> request_context_;
+ base::Thread io_thread_;
+};
+
+// An HttpBridge that doesn't actually make network requests and just calls
+// back with dummy response info.
+class ShuntedHttpBridge : public HttpBridge {
+ public:
+ ShuntedHttpBridge(const URLRequestContext* baseline_context,
+ MessageLoop* io_loop, HttpBridgeTest* test)
+ : HttpBridge(new HttpBridge::RequestContext(baseline_context),
+ io_loop), test_(test) { }
+ protected:
+ virtual void MakeAsynchronousPost() {
+ ASSERT_TRUE(MessageLoop::current() == test_->io_thread_loop());
+ // We don't actually want to make a request for this test, so just callback
+ // as if it completed.
+ test_->io_thread_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &ShuntedHttpBridge::CallOnURLFetchComplete));
+ }
+ private:
+ void CallOnURLFetchComplete() {
+ ASSERT_TRUE(MessageLoop::current() == test_->io_thread_loop());
+ // We return one cookie and a dummy content response.
+ ResponseCookies cookies;
+ cookies.push_back("cookie1");
+ std::string response_content = "success!";
+ OnURLFetchComplete(NULL, GURL("www.google.com"), URLRequestStatus(),
+ 200, cookies, response_content);
+ }
+ HttpBridgeTest* test_;
+};
+
+// Test the HttpBridge without actually making any network requests.
+TEST_F(HttpBridgeTest, TestMakeSynchronousPostShunted) {
+ scoped_refptr<HttpBridge> http_bridge(new ShuntedHttpBridge(
+ new TestURLRequestContext(), io_thread_loop(), this));
+ http_bridge->SetUserAgent("bob");
+ http_bridge->SetURL("http://www.google.com", 9999);
+ http_bridge->SetPostPayload("text/plain", 2, " ");
+
+ int os_error = 0;
+ int response_code = 0;
+ bool success = http_bridge->MakeSynchronousPost(&os_error, &response_code);
+ EXPECT_TRUE(success);
+ EXPECT_EQ(200, response_code);
+ EXPECT_EQ(0, os_error);
+ EXPECT_EQ(1, http_bridge->GetResponseCookieCount());
+ // TODO(timsteele): This is a valid test condition, it's just temporarily
+ // broken so that HttpBridge satisfies the ServerConnectionManager.
+#if FIXED_SYNC_BACKEND_COOKIE_PARSING
+ EXPECT_EQ(std::string("cookie1"),
+ std::string(http_bridge->GetResponseCookieAt(0)));
+#endif
+ EXPECT_EQ(8, http_bridge->GetResponseContentLength());
+ EXPECT_EQ(std::string("success!"),
+ std::string(http_bridge->GetResponseContent()));
+}
+
+// Full round-trip test of the HttpBridge, using default UA string and
+// no request cookies.
+TEST_F(HttpBridgeTest, TestMakeSynchronousPostLiveWithPayload) {
+ scoped_refptr<HTTPTestServer> server = HTTPTestServer::CreateServer(kDocRoot,
+ NULL);
+ ASSERT_TRUE(NULL != server.get());
+
+ scoped_refptr<HttpBridge> http_bridge(BuildBridge());
+
+ std::string payload = "this should be echoed back";
+ GURL echo = server->TestServerPage("echo");
+ http_bridge->SetURL(echo.spec().c_str(), echo.IntPort());
+ http_bridge->SetPostPayload("application/x-www-form-urlencoded",
+ payload.length() + 1, payload.c_str());
+ int os_error = 0;
+ int response_code = 0;
+ bool success = http_bridge->MakeSynchronousPost(&os_error, &response_code);
+ EXPECT_TRUE(success);
+ EXPECT_EQ(200, response_code);
+ EXPECT_EQ(0, os_error);
+ EXPECT_EQ(0, http_bridge->GetResponseCookieCount());
+ EXPECT_EQ(payload.length() + 1, http_bridge->GetResponseContentLength());
+ EXPECT_EQ(payload, std::string(http_bridge->GetResponseContent()));
+}
+
+// Full round-trip test of the HttpBridge, using custom UA string and
+// multiple request cookies.
+TEST_F(HttpBridgeTest, TestMakeSynchronousPostLiveComprehensive) {
+ scoped_refptr<HTTPTestServer> server = HTTPTestServer::CreateServer(kDocRoot,
+ NULL);
+ ASSERT_TRUE(NULL != server.get());
+ scoped_refptr<HttpBridge> http_bridge(BuildBridge());
+
+ GURL echo_header = server->TestServerPage("echoall");
+ http_bridge->SetUserAgent("bob");
+ http_bridge->SetURL(echo_header.spec().c_str(), echo_header.IntPort());
+ http_bridge->AddCookieForRequest("foo=bar");
+ http_bridge->AddCookieForRequest("baz=boo");
+ std::string test_payload = "###TEST PAYLOAD###";
+ http_bridge->SetPostPayload("text/html", test_payload.length() + 1,
+ test_payload.c_str());
+
+ int os_error = 0;
+ int response_code = 0;
+ bool success = http_bridge->MakeSynchronousPost(&os_error, &response_code);
+ EXPECT_TRUE(success);
+ EXPECT_EQ(200, response_code);
+ EXPECT_EQ(0, os_error);
+ EXPECT_EQ(0, http_bridge->GetResponseCookieCount());
+ std::string response = http_bridge->GetResponseContent();
+// TODO(timsteele): This is a valid test condition, it's just temporarily
+// broken so that HttpBridge satisfies the ServerConnectionManager; the format
+// seems to be surprising the TestServer, because it isn't echoing the headers
+// properly.
+#if FIXED_SYNCER_BACKEND_COOKIE_PARSING
+ EXPECT_NE(std::string::npos, response.find("Cookie: foo=bar; baz=boo"));
+ EXPECT_NE(std::string::npos, response.find("User-Agent: bob"));
+#endif
+ EXPECT_NE(std::string::npos, response.find(test_payload.c_str()));
+}
+
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/model_associator.cc b/chrome/browser/sync/glue/model_associator.cc
new file mode 100644
index 0000000..3d73612
--- /dev/null
+++ b/chrome/browser/sync/glue/model_associator.cc
@@ -0,0 +1,504 @@
+// Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "chrome/browser/sync/glue/model_associator.h"
+
+#include <stack>
+
+#include "base/message_loop.h"
+#include "base/task.h"
+#include "chrome/browser/bookmarks/bookmark_model.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/profile_sync_service.h"
+
+namespace browser_sync {
+
+// The sync protocol identifies top-level entities by means of well-known tags,
+// which should not be confused with titles. Each tag corresponds to a
+// singleton instance of a particular top-level node in a user's share; the
+// tags are consistent across users. The tags allow us to locate the specific
+// folders whose contents we care about synchronizing, without having to do a
+// lookup by name or path. The tags should not be made user-visible.
+// For example, the tag "bookmark_bar" represents the permanent node for
+// bookmarks bar in Chrome. The tag "other_bookmarks" represents the permanent
+// folder Other Bookmarks in Chrome.
+//
+// It is the responsibility of something upstream (at time of writing,
+// the sync server) to create these tagged nodes when initializing sync
+// for the first time for a user. Thus, once the backend finishes
+// initializing, the ProfileSyncService can rely on the presence of tagged
+// nodes.
+//
+// TODO(ncarter): Pull these tags from an external protocol specification
+// rather than hardcoding them here.
+static const wchar_t* kOtherBookmarksTag = L"other_bookmarks";
+static const wchar_t* kBookmarkBarTag = L"bookmark_bar";
+
+// Bookmark comparer for map of bookmark nodes.
+class BookmarkComparer {
+ public:
+ // Compares the two given nodes and returns whether node1 should appear
+ // before node2 in strict weak ordering.
+ bool operator()(const BookmarkNode* node1,
+ const BookmarkNode* node2) const {
+ DCHECK(node1);
+ DCHECK(node2);
+
+ // Keep folder nodes before non-folder nodes.
+ if (node1->is_folder() != node2->is_folder())
+ return node1->is_folder();
+
+ int result = node1->GetTitle().compare(node2->GetTitle());
+ if (result != 0)
+ return result < 0;
+
+ result = node1->GetURL().spec().compare(node2->GetURL().spec());
+ if (result != 0)
+ return result < 0;
+
+ return false;
+ }
+};
+
+// Provides the following abstraction: given a parent bookmark node, find best
+// matching child node for many sync nodes.
+class BookmarkNodeFinder {
+ public:
+ // Creats an instance with the given parent bookmark node.
+ explicit BookmarkNodeFinder(const BookmarkNode* parent_node);
+
+ // Finds best matching node for the given sync node.
+ // Returns the matching node if one exists; NULL otherwise. If a matching
+ // node is found, it's removed for further matches.
+ const BookmarkNode* FindBookmarkNode(const sync_api::BaseNode& sync_node);
+
+ private:
+ typedef std::set<const BookmarkNode*, BookmarkComparer> BookmarkNodesSet;
+
+ const BookmarkNode* parent_node_;
+ BookmarkNodesSet child_nodes_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkNodeFinder);
+};
+
+BookmarkNodeFinder::BookmarkNodeFinder(const BookmarkNode* parent_node)
+ : parent_node_(parent_node) {
+ for (int i = 0; i < parent_node_->GetChildCount(); ++i)
+ child_nodes_.insert(parent_node_->GetChild(i));
+}
+
+const BookmarkNode* BookmarkNodeFinder::FindBookmarkNode(
+ const sync_api::BaseNode& sync_node) {
+ // Create a bookmark node from the given sync node.
+ BookmarkNode temp_node(GURL(sync_node.GetURL()));
+ temp_node.SetTitle(UTF16ToWide(sync_node.GetTitle()));
+ if (sync_node.GetIsFolder())
+ temp_node.SetType(BookmarkNode::FOLDER);
+ else
+ temp_node.SetType(BookmarkNode::URL);
+
+ const BookmarkNode* result = NULL;
+ BookmarkNodesSet::iterator iter = child_nodes_.find(&temp_node);
+ if (iter != child_nodes_.end()) {
+ result = *iter;
+ // Remove the matched node so we don't match with it again.
+ child_nodes_.erase(iter);
+ }
+
+ return result;
+}
+
+ModelAssociator::ModelAssociator(ProfileSyncService* sync_service)
+ : sync_service_(sync_service),
+ task_pending_(false) {
+ DCHECK(sync_service_);
+}
+
+void ModelAssociator::ClearAll() {
+ id_map_.clear();
+ id_map_inverse_.clear();
+ dirty_assocations_sync_ids_.clear();
+}
+
+int64 ModelAssociator::GetSyncIdFromBookmarkId(int64 node_id) const {
+ BookmarkIdToSyncIdMap::const_iterator iter = id_map_.find(node_id);
+ return iter == id_map_.end() ? sync_api::kInvalidId : iter->second;
+}
+
+bool ModelAssociator::GetBookmarkIdFromSyncId(int64 sync_id,
+ int64* node_id) const {
+ SyncIdToBookmarkIdMap::const_iterator iter = id_map_inverse_.find(sync_id);
+ if (iter == id_map_inverse_.end())
+ return false;
+ *node_id = iter->second;
+ return true;
+}
+
+bool ModelAssociator::InitSyncNodeFromBookmarkId(
+ int64 node_id,
+ sync_api::BaseNode* sync_node) {
+ DCHECK(sync_node);
+ int64 sync_id = GetSyncIdFromBookmarkId(node_id);
+ if (sync_id == sync_api::kInvalidId)
+ return false;
+ if (!sync_node->InitByIdLookup(sync_id))
+ return false;
+ DCHECK(sync_node->GetId() == sync_id);
+ return true;
+}
+
+const BookmarkNode* ModelAssociator::GetBookmarkNodeFromSyncId(int64 sync_id) {
+ int64 node_id;
+ if (!GetBookmarkIdFromSyncId(sync_id, &node_id))
+ return false;
+ BookmarkModel* model = sync_service_->profile()->GetBookmarkModel();
+ return model->GetNodeByID(node_id);
+}
+
+void ModelAssociator::AssociateIds(int64 node_id, int64 sync_id) {
+ DCHECK_NE(sync_id, sync_api::kInvalidId);
+ DCHECK(id_map_.find(node_id) == id_map_.end());
+ DCHECK(id_map_inverse_.find(sync_id) == id_map_inverse_.end());
+ id_map_[node_id] = sync_id;
+ id_map_inverse_[sync_id] = node_id;
+ dirty_assocations_sync_ids_.insert(sync_id);
+ PostPersistAssociationsTask();
+}
+
+void ModelAssociator::DisassociateIds(int64 sync_id) {
+ SyncIdToBookmarkIdMap::iterator iter = id_map_inverse_.find(sync_id);
+ if (iter == id_map_inverse_.end())
+ return;
+ id_map_.erase(iter->second);
+ id_map_inverse_.erase(iter);
+ dirty_assocations_sync_ids_.erase(sync_id);
+}
+
+bool ModelAssociator::BookmarkModelHasUserCreatedNodes() const {
+ BookmarkModel* model = sync_service_->profile()->GetBookmarkModel();
+ DCHECK(model->IsLoaded());
+ return model->GetBookmarkBarNode()->GetChildCount() > 0 ||
+ model->other_node()->GetChildCount() > 0;
+}
+
+bool ModelAssociator::SyncModelHasUserCreatedNodes() {
+ int64 bookmark_bar_sync_id;
+ if (!GetSyncIdForTaggedNode(WideToUTF16(kBookmarkBarTag),
+ &bookmark_bar_sync_id)) {
+ NOTREACHED();
+ return false;
+ }
+ int64 other_bookmarks_sync_id;
+ if (!GetSyncIdForTaggedNode(WideToUTF16(kOtherBookmarksTag),
+ &other_bookmarks_sync_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ sync_api::ReadTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+
+ sync_api::ReadNode bookmark_bar_node(&trans);
+ if (!bookmark_bar_node.InitByIdLookup(bookmark_bar_sync_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ sync_api::ReadNode other_bookmarks_node(&trans);
+ if (!other_bookmarks_node.InitByIdLookup(other_bookmarks_sync_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ // Sync model has user created nodes if either one of the permanent nodes
+ // has children.
+ return bookmark_bar_node.GetFirstChildId() != sync_api::kInvalidId ||
+ other_bookmarks_node.GetFirstChildId() != sync_api::kInvalidId;
+}
+
+bool ModelAssociator::NodesMatch(const BookmarkNode* bookmark,
+ const sync_api::BaseNode* sync_node) const {
+ if (bookmark->GetTitle() != UTF16ToWide(sync_node->GetTitle()))
+ return false;
+ if (bookmark->is_folder() != sync_node->GetIsFolder())
+ return false;
+ if (bookmark->is_url()) {
+ if (bookmark->GetURL() != GURL(sync_node->GetURL()))
+ 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 ModelAssociator::AssociateTaggedPermanentNode(
+ const BookmarkNode* permanent_node,
+ const string16 &tag) {
+ // Do nothing if |permanent_node| is already initialized and associated.
+ int64 sync_id = GetSyncIdFromBookmarkId(permanent_node->id());
+ if (sync_id != sync_api::kInvalidId)
+ return true;
+ if (!GetSyncIdForTaggedNode(tag, &sync_id))
+ return false;
+
+ AssociateIds(permanent_node->id(), sync_id);
+ return true;
+}
+
+bool ModelAssociator::GetSyncIdForTaggedNode(const string16& tag,
+ int64* sync_id) {
+ sync_api::ReadTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+ sync_api::ReadNode sync_node(&trans);
+ if (!sync_node.InitByTagLookup(tag.c_str()))
+ return false;
+ *sync_id = sync_node.GetId();
+ return true;
+}
+
+bool ModelAssociator::AssociateModels() {
+ // Try to load model associations from persisted associations first. If that
+ // succeeds, we don't need to run the complex model matching algorithm.
+ if (LoadAssociations())
+ return true;
+
+ ClearAll();
+
+ // We couldn't load model assocations from persisted assocations. So build
+ // them.
+ return BuildAssocations();
+}
+
+bool ModelAssociator::BuildAssocations() {
+ // Algorithm description:
+ // Match up the roots and recursively do the following:
+ // * For each sync node for the current sync parent node, find the best
+ // matching bookmark node under the corresponding bookmark parent node.
+ // If no matching node is found, create a new bookmark node in the same
+ // position as the corresponding sync node.
+ // If a matching node is found, update the properties of it from the
+ // corresponding sync node.
+ // * 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.
+
+ BookmarkModel* model = sync_service_->profile()->GetBookmarkModel();
+ DCHECK(model->IsLoaded());
+
+ // To prime our association, we associate the top-level nodes, Bookmark Bar
+ // and Other Bookmarks.
+ if (!AssociateTaggedPermanentNode(model->other_node(),
+ WideToUTF16(kOtherBookmarksTag))) {
+ NOTREACHED() << "Server did not create top-level nodes. Possibly we "
+ << "are running against an out-of-date server?";
+ return false;
+ }
+ if (!AssociateTaggedPermanentNode(model->GetBookmarkBarNode(),
+ WideToUTF16(kBookmarkBarTag))) {
+ NOTREACHED() << "Server did not create top-level nodes. Possibly we "
+ << "are running against an out-of-date server?";
+ return false;
+ }
+ int64 bookmark_bar_sync_id = GetSyncIdFromBookmarkId(
+ model->GetBookmarkBarNode()->id());
+ DCHECK(bookmark_bar_sync_id != sync_api::kInvalidId);
+ int64 other_bookmarks_sync_id = GetSyncIdFromBookmarkId(
+ model->other_node()->id());
+ DCHECK(other_bookmarks_sync_id!= sync_api::kInvalidId);
+
+ std::stack<int64> dfs_stack;
+ dfs_stack.push(other_bookmarks_sync_id);
+ dfs_stack.push(bookmark_bar_sync_id);
+
+ sync_api::WriteTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+
+ while (!dfs_stack.empty()) {
+ int64 sync_parent_id = dfs_stack.top();
+ dfs_stack.pop();
+
+ sync_api::ReadNode sync_parent(&trans);
+ if (!sync_parent.InitByIdLookup(sync_parent_id)) {
+ NOTREACHED();
+ return false;
+ }
+ // Only folder nodes are pushed on to the stack.
+ DCHECK(sync_parent.GetIsFolder());
+
+ const BookmarkNode* parent_node = GetBookmarkNodeFromSyncId(sync_parent_id);
+ DCHECK(parent_node->is_folder());
+
+ BookmarkNodeFinder node_finder(parent_node);
+
+ int index = 0;
+ int64 sync_child_id = sync_parent.GetFirstChildId();
+ while (sync_child_id != sync_api::kInvalidId) {
+ sync_api::WriteNode sync_child_node(&trans);
+ if (!sync_child_node.InitByIdLookup(sync_child_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ const BookmarkNode* child_node = NULL;
+ child_node = node_finder.FindBookmarkNode(sync_child_node);
+ if (child_node) {
+ model->Move(child_node, parent_node, index);
+ // Set the favicon for bookmark node from sync node or vice versa.
+ if (!sync_service_->SetBookmarkFavicon(&sync_child_node, child_node))
+ sync_service_->SetSyncNodeFavicon(child_node, &sync_child_node);
+ } else {
+ // Create a new bookmark node for the sync node.
+ child_node = sync_service_->CreateBookmarkNode(&sync_child_node,
+ parent_node,
+ index);
+ }
+ AssociateIds(child_node->id(), sync_child_id);
+ if (sync_child_node.GetIsFolder())
+ dfs_stack.push(sync_child_id);
+
+ sync_child_id = sync_child_node.GetSuccessorId();
+ ++index;
+ }
+
+ // At this point all the children nodes of the parent sync node have
+ // corresponding children in the parent bookmark node and they are all in
+ // the right positions: from 0 to index - 1.
+ // So the children starting from index in the parent bookmark node are the
+ // ones that are not present in the parent sync node. So create them.
+ for (int i = index; i < parent_node->GetChildCount(); ++i) {
+ sync_child_id = sync_service_->CreateSyncNode(parent_node, i, &trans);
+ if (parent_node->GetChild(i)->is_folder())
+ dfs_stack.push(sync_child_id);
+ }
+ }
+ return true;
+}
+
+void ModelAssociator::PostPersistAssociationsTask() {
+ // No need to post a task if a task is already pending.
+ if (task_pending_)
+ return;
+ task_pending_ = true;
+ MessageLoop::current()->PostTask(
+ FROM_HERE,
+ NewRunnableMethod(this, &ModelAssociator::PersistAssociations));
+}
+
+void ModelAssociator::PersistAssociations() {
+ DCHECK(task_pending_);
+ task_pending_ = false;
+
+ // If there are no dirty assocations we have nothing to do. We handle this
+ // explicity instead of letting the for loop do it to avoid creating a write
+ // transaction in this case.
+ if (dirty_assocations_sync_ids_.empty()) {
+ DCHECK(id_map_.empty());
+ DCHECK(id_map_inverse_.empty());
+ return;
+ }
+
+ sync_api::WriteTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+ DirtyAssocationsSyncIds::iterator iter;
+ for (iter = dirty_assocations_sync_ids_.begin();
+ iter != dirty_assocations_sync_ids_.end();
+ ++iter) {
+ int64 sync_id = *iter;
+ sync_api::WriteNode sync_node(&trans);
+ if (!sync_node.InitByIdLookup(sync_id)) {
+ sync_service_->SetUnrecoverableError();
+ return;
+ }
+ int64 node_id;
+ if (GetBookmarkIdFromSyncId(sync_id, &node_id))
+ sync_node.SetExternalId(node_id);
+ else
+ NOTREACHED();
+ }
+ dirty_assocations_sync_ids_.clear();
+}
+
+bool ModelAssociator::LoadAssociations() {
+ BookmarkModel* model = sync_service_->profile()->GetBookmarkModel();
+ DCHECK(model->IsLoaded());
+ // If the bookmarks changed externally, our previous assocations may not be
+ // valid; so return false.
+ if (model->file_changed())
+ return false;
+
+ // Our persisted assocations should be valid. Try to populate id assocation
+ // maps using persisted assocations.
+
+ int64 other_bookmarks_id;
+ if (!GetSyncIdForTaggedNode(WideToUTF16(kOtherBookmarksTag),
+ &other_bookmarks_id)) {
+ NOTREACHED(); // We should always be able to find the permanent nodes.
+ return false;
+ }
+ int64 bookmark_bar_id;
+ if (!GetSyncIdForTaggedNode(WideToUTF16(kBookmarkBarTag), &bookmark_bar_id)) {
+ NOTREACHED(); // We should always be able to find the permanent nodes.
+ return false;
+ }
+
+ std::stack<int64> dfs_stack;
+ dfs_stack.push(other_bookmarks_id);
+ dfs_stack.push(bookmark_bar_id);
+
+ sync_api::ReadTransaction trans(
+ sync_service_->backend()->GetUserShareHandle());
+
+ while (!dfs_stack.empty()) {
+ int64 parent_id = dfs_stack.top();
+ dfs_stack.pop();
+ sync_api::ReadNode sync_parent(&trans);
+ if (!sync_parent.InitByIdLookup(parent_id)) {
+ NOTREACHED();
+ return false;
+ }
+
+ int64 external_id = sync_parent.GetExternalId();
+ if (external_id == 0)
+ return false;
+
+ const BookmarkNode* node = model->GetNodeByID(external_id);
+ if (!node)
+ return false;
+
+ // Don't try to call NodesMatch on permanent nodes like bookmark bar and
+ // other bookmarks. They are not expected to match.
+ if (node != model->GetBookmarkBarNode() &&
+ node != model->other_node() &&
+ !NodesMatch(node, &sync_parent))
+ return false;
+
+ AssociateIds(external_id, sync_parent.GetId());
+
+ // Add all children of the current node to the stack.
+ int64 child_id = sync_parent.GetFirstChildId();
+ while (child_id != sync_api::kInvalidId) {
+ dfs_stack.push(child_id);
+ sync_api::ReadNode child_node(&trans);
+ if (!child_node.InitByIdLookup(child_id)) {
+ NOTREACHED();
+ return false;
+ }
+ child_id = child_node.GetSuccessorId();
+ }
+ }
+ DCHECK(dfs_stack.empty());
+ return true;
+}
+
+} // namespace browser_sync
+
+#endif // CHROME_PERSONALIZATION
diff --git a/chrome/browser/sync/glue/model_associator.h b/chrome/browser/sync/glue/model_associator.h
new file mode 100644
index 0000000..a81f337
--- /dev/null
+++ b/chrome/browser/sync/glue/model_associator.h
@@ -0,0 +1,141 @@
+// Copyright (c) 2006-2009 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_GLUE_MODEL_ASSOCATOR_H_
+#define CHROME_BROWSER_SYNC_GLUE_MODEL_ASSOCATOR_H_
+
+#include <map>
+#include <set>
+#include <string>
+
+#include "base/basictypes.h"
+#include "base/ref_counted.h"
+#include "base/scoped_ptr.h"
+#include "base/string16.h"
+
+class BookmarkNode;
+
+namespace sync_api {
+class BaseNode;
+class BaseTransaction;
+class ReadNode;
+};
+
+class ProfileSyncService;
+
+namespace browser_sync {
+
+// Contains all model assocation related logic:
+// * Algorithm to associate bookmark model and sync model.
+// * Methods to get a bookmark node for a given sync node and vice versa.
+// * Persisting model assocations and loading them back.
+class ModelAssociator
+ : public base::RefCountedThreadSafe<ModelAssociator> {
+ public:
+ explicit ModelAssociator(ProfileSyncService* sync_service);
+ virtual ~ModelAssociator() { }
+
+ // Clears all assocations .
+ void ClearAll();
+
+ // Returns sync id for the given bookmark node id.
+ // Returns sync_api::kInvalidId if the sync node is not found for the given
+ // bookmark node id.
+ int64 GetSyncIdFromBookmarkId(int64 node_id) const;
+
+ // Stores bookmark node id for the given sync id in bookmark_id. Returns true
+ // if the bookmark id was successfully found; false otherwise.
+ bool GetBookmarkIdFromSyncId(int64 sync_id, int64* bookmark_id) const;
+
+ // Initializes the given sync node from the given bookmark node id.
+ // Returns false if no sync node was found for the given bookmark node id or
+ // if the initialization of sync node fails.
+ bool InitSyncNodeFromBookmarkId(int64 node_id, sync_api::BaseNode* sync_node);
+
+ // Returns the bookmark node for the given sync id.
+ // Returns NULL if no bookmark node is found for the given sync id.
+ const BookmarkNode* GetBookmarkNodeFromSyncId(int64 sync_id);
+
+ // Associates the given bookmark node id with the given sync id.
+ void AssociateIds(int64 node_id, int64 sync_id);
+ // Disassociate the ids that correspond to the given sync id.
+ void DisassociateIds(int64 sync_id);
+
+ // Returns whether the bookmark model has user created nodes or not. That is,
+ // whether there are nodes in the bookmark model except the bookmark bar and
+ // other bookmarks.
+ bool BookmarkModelHasUserCreatedNodes() const;
+
+ // Returns whether the sync model has nodes other than the permanent tagged
+ // nodes.
+ bool SyncModelHasUserCreatedNodes();
+
+ // AssociateModels iterates through both the sync and the browser
+ // bookmark model, looking for matched pairs of items. For any pairs it
+ // finds, it will call AssociateSyncID. For any unmatched items,
+ // MergeAndAssociateModels will try to repair the match, e.g. by adding a new
+ // node. After successful completion, the models should be identical and
+ // corresponding. Returns true on success. On failure of this step, we
+ // should abort the sync operation and report an error to the user.
+ bool AssociateModels();
+
+ protected:
+ // Stores the id of the node with the given tag in |sync_id|.
+ // Returns of that node was found successfully.
+ // Tests override this.
+ virtual bool GetSyncIdForTaggedNode(const string16& tag, int64* sync_id);
+
+ // Returns sync service instance.
+ ProfileSyncService* sync_service() { return sync_service_; }
+
+ private:
+ typedef std::map<int64, int64> BookmarkIdToSyncIdMap;
+ typedef std::map<int64, int64> SyncIdToBookmarkIdMap;
+ typedef std::set<int64> DirtyAssocationsSyncIds;
+
+ // Posts a task to persist dirty assocations.
+ void PostPersistAssociationsTask();
+ // Persists all dirty assocations.
+ void PersistAssociations();
+
+ // Loads the persisted assocations into in-memory maps.
+ // If the persisted associations are out-of-date due to some reason, returns
+ // false; otehrwise returns true.
+ bool LoadAssociations();
+
+ // Matches up the bookmark model and the sync model to build model
+ // assocations.
+ bool BuildAssocations();
+
+ // Associate a top-level node of the bookmark model with a permanent node in
+ // the sync domain. Such permanent nodes are identified by a tag that is
+ // well known to the server and the client, and is unique within a particular
+ // user's share. For example, "other_bookmarks" is the tag for the Other
+ // Bookmarks folder. The sync nodes are server-created.
+ bool AssociateTaggedPermanentNode(const BookmarkNode* permanent_node,
+ const string16& tag);
+
+ // Compare the properties of a pair of nodes from either domain.
+ bool NodesMatch(const BookmarkNode* bookmark,
+ const sync_api::BaseNode* sync_node) const;
+
+ ProfileSyncService* sync_service_;
+ BookmarkIdToSyncIdMap id_map_;
+ SyncIdToBookmarkIdMap id_map_inverse_;
+ // Stores sync ids for dirty associations.
+ DirtyAssocationsSyncIds dirty_assocations_sync_ids_;
+
+ // Indicates whether there is already a pending task to persist dirty model
+ // associations.
+ bool task_pending_;
+
+ DISALLOW_COPY_AND_ASSIGN(ModelAssociator);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_GLUE_MODEL_ASSOCATOR_H_
+#endif // CHROME_PERSONALIZATION \ No newline at end of file
diff --git a/chrome/browser/sync/glue/sync_backend_host.cc b/chrome/browser/sync/glue/sync_backend_host.cc
new file mode 100644
index 0000000..4614ee3
--- /dev/null
+++ b/chrome/browser/sync/glue/sync_backend_host.cc
@@ -0,0 +1,308 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#include "base/file_version_info.h"
+#include "base/file_util.h"
+#include "base/string_util.h"
+#include "chrome/browser/sync/glue/sync_backend_host.h"
+#include "chrome/browser/sync/glue/http_bridge.h"
+#include "chrome/browser/sync/glue/bookmark_model_worker.h"
+#include "webkit/glue/webkit_glue.h"
+
+static const char kSwitchSyncServiceURL[] = "sync-url";
+static const char kSwitchSyncServicePort[] = "sync-port";
+static const int kSaveChangesIntervalSeconds = 10;
+static const char kGaiaServiceId[] = "chromiumsync";
+static const char kGaiaSourceForChrome[] = "ChromiumBrowser";
+static const FilePath::CharType kSyncDataFolderName[] =
+ FILE_PATH_LITERAL("Sync Data");
+
+namespace browser_sync {
+
+SyncBackendHost::SyncBackendHost(SyncFrontend* frontend,
+ const FilePath& profile_path)
+ : core_thread_("Chrome_SyncCoreThread"),
+ frontend_loop_(MessageLoop::current()),
+ bookmark_model_worker_(NULL),
+ frontend_(frontend),
+ sync_data_folder_path_(profile_path.Append(kSyncDataFolderName)),
+ last_auth_error_(AUTH_ERROR_NONE) {
+ core_ = new Core(this);
+}
+
+SyncBackendHost::~SyncBackendHost() {
+ DCHECK(!core_ && !frontend_) << "Must call Shutdown before destructor.";
+}
+
+void SyncBackendHost::Initialize(const GURL& sync_service_url) {
+ if (!core_thread_.Start())
+ return;
+
+ bookmark_model_worker_ = new BookmarkModelWorker(frontend_loop_);
+ core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(core_.get(), &SyncBackendHost::Core::DoInitialize,
+ sync_service_url, bookmark_model_worker_, true));
+}
+
+void SyncBackendHost::Authenticate(const std::string& username,
+ const std::string& password) {
+ core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(core_.get(), &SyncBackendHost::Core::DoAuthenticate,
+ username, password));
+}
+
+void SyncBackendHost::Shutdown(bool sync_disabled) {
+ // Thread shutdown should occur in the following order:
+ // - SyncerThread
+ // - CoreThread
+ // - UI Thread (stops some time after we return from this call).
+ core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(core_.get(),
+ &SyncBackendHost::Core::DoShutdown,
+ sync_disabled));
+
+ // Before joining the core_thread_, we wait for the BookmarkModelWorker to
+ // give us the green light that it is not depending on the frontend_loop_ to
+ // process any more tasks. Stop() blocks until this termination condition
+ // is true.
+ bookmark_model_worker_->Stop();
+
+ // Stop will return once the thread exits, which will be after DoShutdown
+ // runs. DoShutdown needs to run from core_thread_ because the sync backend
+ // requires any thread that opened sqlite handles to relinquish them
+ // personally. We need to join threads, because otherwise the main Chrome
+ // thread (ui loop) can exit before DoShutdown finishes, at which point
+ // virtually anything the sync backend does (or the post-back to
+ // frontend_loop_ by our Core) will epically fail because the CRT won't be
+ // initialized. For now this only ever happens at sync-enabled-Chrome exit,
+ // meaning bug 1482548 applies to prolonged "waiting" that may occur in
+ // DoShutdown.
+ core_thread_.Stop();
+
+ bookmark_model_worker_ = NULL;
+ frontend_ = NULL;
+ core_ = NULL; // Releases reference to core_.
+}
+
+void SyncBackendHost::Core::NotifyFrontend(FrontendNotification notification) {
+ if (!host_ || !host_->frontend_) {
+ return; // This can happen in testing because the UI loop processes tasks
+ // after an instance of SyncBackendHost was destroyed. In real
+ // life this doesn't happen.
+ }
+ switch(notification) {
+ case INITIALIZED:
+ host_->frontend_->OnBackendInitialized();
+ return;
+ case SYNC_CYCLE_COMPLETED:
+ host_->frontend_->OnSyncCycleCompleted();
+ return;
+ }
+}
+
+SyncBackendHost::UserShareHandle SyncBackendHost::GetUserShareHandle() const {
+ return core_->syncapi()->GetUserShare();
+}
+
+SyncBackendHost::Status SyncBackendHost::GetDetailedStatus() {
+ return core_->syncapi()->GetDetailedStatus();
+}
+
+SyncBackendHost::StatusSummary SyncBackendHost::GetStatusSummary() {
+ return core_->syncapi()->GetStatusSummary();
+}
+
+string16 SyncBackendHost::GetAuthenticatedUsername() const {
+ return UTF8ToUTF16(core_->syncapi()->GetAuthenticatedUsername());
+}
+
+AuthErrorState SyncBackendHost::GetAuthErrorState() const {
+ return last_auth_error_;
+}
+
+SyncBackendHost::Core::Core(SyncBackendHost* backend)
+ : host_(backend),
+ syncapi_(new sync_api::SyncManager()) {
+}
+
+// Helper to construct a user agent string (ASCII) suitable for use by
+// the syncapi for any HTTP communication. This string is used by the sync
+// backend for classifying client types when calculating statistics.
+std::string MakeUserAgentForSyncapi() {
+ std::string user_agent;
+ user_agent = "Chrome ";
+#if defined (OS_WIN)
+ user_agent += "WIN ";
+#elif defined (OS_LINUX)
+ user_agent += "LINUX ";
+#elif defined (OS_MACOSX)
+ user_agent += "MAC ";
+#endif
+ scoped_ptr<FileVersionInfo> version_info(
+ FileVersionInfo::CreateFileVersionInfoForCurrentModule());
+ if (version_info == NULL) {
+ DLOG(ERROR) << "Unable to create FileVersionInfo object";
+ return user_agent;
+ }
+
+ user_agent += WideToASCII(version_info->product_version());
+ user_agent += " (" + WideToASCII(version_info->last_change()) + ")";
+ if (!version_info->is_official_build())
+ user_agent += "-devel";
+ return user_agent;
+}
+
+void SyncBackendHost::Core::DoInitialize(
+ const GURL& service_url,
+ BookmarkModelWorker* bookmark_model_worker,
+ bool attempt_last_user_authentication) {
+ DCHECK(MessageLoop::current() == host_->core_thread_.message_loop());
+
+ // Make sure that the directory exists before initializing the backend.
+ // If it already exists, this will do no harm.
+ bool success = file_util::CreateDirectory(host_->sync_data_folder_path());
+ DCHECK(success);
+
+ syncapi_->SetObserver(this);
+ string16 path_str;
+#if defined (OS_WIN)
+ path_str = host_->sync_data_folder_path().value();
+#elif (defined (OS_LINUX) || defined (OS_MACOSX))
+ path_str = UTF8ToUTF16(sync_data_folder_path().value());
+#endif
+ success = syncapi_->Init(path_str.c_str(),
+ (service_url.host() + service_url.path()).c_str(),
+ service_url.EffectiveIntPort(),
+ kGaiaServiceId,
+ kGaiaSourceForChrome,
+ service_url.SchemeIsSecure(),
+ new HttpBridgeFactory(),
+ new HttpBridgeFactory(),
+ bookmark_model_worker,
+ attempt_last_user_authentication,
+ MakeUserAgentForSyncapi().c_str());
+ DCHECK(success) << "Syncapi initialization failed!";
+}
+
+void SyncBackendHost::Core::DoAuthenticate(const std::string& username,
+ const std::string& password) {
+ DCHECK(MessageLoop::current() == host_->core_thread_.message_loop());
+ syncapi_->Authenticate(username.c_str(), password.c_str());
+}
+
+void SyncBackendHost::Core::DoShutdown(bool sync_disabled) {
+ DCHECK(MessageLoop::current() == host_->core_thread_.message_loop());
+
+ save_changes_timer_.Stop();
+ syncapi_->Shutdown(); // Stops the SyncerThread.
+ syncapi_->RemoveObserver();
+ host_->bookmark_model_worker_->OnSyncerShutdownComplete();
+
+ if (sync_disabled &&
+ file_util::DirectoryExists(host_->sync_data_folder_path())) {
+ // Delete the sync data folder to cleanup backend data.
+ bool success = file_util::Delete(host_->sync_data_folder_path(), true);
+ DCHECK(success);
+ }
+
+ host_ = NULL;
+}
+
+static AuthErrorState AuthProblemToAuthError(
+ const sync_api::SyncManager::AuthProblem& auth_problem) {
+ switch(auth_problem) {
+ case sync_api::SyncManager::AUTH_PROBLEM_NONE:
+ return AUTH_ERROR_NONE;
+ case sync_api::SyncManager::AUTH_PROBLEM_INVALID_GAIA_CREDENTIALS:
+ return AUTH_ERROR_INVALID_GAIA_CREDENTIALS;
+ case sync_api::SyncManager::AUTH_PROBLEM_CONNECTION_FAILED:
+ return AUTH_ERROR_CONNECTION_FAILED;
+ case sync_api::SyncManager::AUTH_PROBLEM_USER_NOT_SIGNED_UP:
+ return AUTH_ERROR_USER_NOT_SIGNED_UP;
+ }
+
+ NOTREACHED() << "Unknown AuthProblem.";
+ return AUTH_ERROR_NONE;
+}
+
+void SyncBackendHost::Core::OnChangesApplied(
+ const sync_api::BaseTransaction* trans,
+ const sync_api::SyncManager::ChangeRecord* changes,
+ int change_count) {
+ if (!host_ || !host_->frontend_) {
+ DCHECK(false) << "OnChangesApplied called after Shutdown?";
+ return;
+ }
+
+ // ChangesApplied is the one exception that should come over from the sync
+ // backend already on the service_loop_ thanks to our BookmarkModelWorker.
+ // SyncFrontend changes exclusively on the UI loop, because it updates
+ // the bookmark model. As such, we don't need to worry about changes that
+ // have been made to the bookmark model but not yet applied to the sync
+ // model -- such changes only happen on the UI loop, and there's no
+ // contention.
+ if (host_->frontend_loop_ != MessageLoop::current()) {
+ // TODO(ncarter): Bug 1480644. Make this a DCHECK once syncapi filters
+ // out all irrelevant changes.
+ DLOG(WARNING) << "Could not update bookmark model from non-UI thread";
+ return;
+ }
+ host_->frontend_->ApplyModelChanges(trans, changes, change_count);
+}
+
+void SyncBackendHost::Core::OnSyncCycleCompleted() {
+ host_->frontend_loop_->PostTask(FROM_HERE, NewRunnableMethod(this,
+ &Core::NotifyFrontend, SYNC_CYCLE_COMPLETED));
+}
+
+void SyncBackendHost::Core::OnInitializationComplete() {
+ if (!host_ || !host_->frontend_)
+ return; // We may have been told to Shutdown before initialization
+ // completed.
+
+ // We could be on some random sync backend thread, so MessageLoop::current()
+ // can definitely be null in here.
+ host_->frontend_loop_->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &Core::NotifyFrontend, INITIALIZED));
+
+ // Initialization is complete, so we can schedule recurring SaveChanges.
+ host_->core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &Core::StartSavingChanges));
+}
+
+void SyncBackendHost::Core::OnAuthProblem(
+ sync_api::SyncManager::AuthProblem auth_problem) {
+ // We could be on SyncEngine_AuthWatcherThread. Post to our core loop so
+ // we can modify state.
+ host_->frontend_loop_->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &Core::HandleAuthErrorEventOnFrontendLoop,
+ AuthProblemToAuthError(auth_problem)));
+}
+
+void SyncBackendHost::Core::HandleAuthErrorEventOnFrontendLoop(
+ AuthErrorState new_auth_error) {
+ if (!host_ || !host_->frontend_)
+ return;
+
+ DCHECK_EQ(MessageLoop::current(), host_->frontend_loop_);
+
+ host_->last_auth_error_ = new_auth_error;
+ host_->frontend_->OnAuthError();
+}
+
+void SyncBackendHost::Core::StartSavingChanges() {
+ save_changes_timer_.Start(
+ base::TimeDelta::FromSeconds(kSaveChangesIntervalSeconds),
+ this, &Core::SaveChanges);
+}
+
+void SyncBackendHost::Core::SaveChanges() {
+ syncapi_->SaveChanges();
+}
+
+} // namespace browser_sync
+
+#endif
diff --git a/chrome/browser/sync/glue/sync_backend_host.h b/chrome/browser/sync/glue/sync_backend_host.h
new file mode 100644
index 0000000..eb89e43
--- /dev/null
+++ b/chrome/browser/sync/glue/sync_backend_host.h
@@ -0,0 +1,274 @@
+// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifdef CHROME_PERSONALIZATION
+
+#ifndef CHROME_BROWSER_SYNC_GLUE_SYNC_BACKEND_HOST_H_
+#define CHROME_BROWSER_SYNC_GLUE_SYNC_BACKEND_HOST_H_
+
+#include <string>
+
+#include "base/file_path.h"
+#include "base/lock.h"
+#include "base/message_loop.h"
+#include "base/ref_counted.h"
+#include "base/thread.h"
+#include "base/timer.h"
+#include "chrome/browser/sync/auth_error_state.h"
+#include "chrome/browser/sync/engine/syncapi.h"
+#include "chrome/browser/sync/glue/bookmark_model_worker.h"
+#include "googleurl/src/gurl.h"
+
+namespace browser_sync {
+
+class SyncFrontend;
+
+// A UI-thread safe API into the sync backend that "hosts" the top-level
+// syncapi element, the SyncManager, on its own thread. This class handles
+// dispatch of potentially blocking calls to appropriate threads and ensures
+// that the SyncFrontend is only accessed on the UI loop.
+class SyncBackendHost {
+ public:
+ typedef sync_api::UserShare* UserShareHandle;
+ typedef sync_api::SyncManager::Status::Summary StatusSummary;
+ typedef sync_api::SyncManager::Status Status;
+
+ // Create a SyncBackendHost with a reference to the |frontend| that it serves
+ // and communicates to via the SyncFrontend interface (on the same thread
+ // it used to call the constructor).
+ SyncBackendHost(SyncFrontend* frontend, const FilePath& proifle_path);
+ ~SyncBackendHost();
+
+ // Called on |frontend_loop_| to kick off asynchronous initialization.
+ void Initialize(const GURL& service_url);
+
+ // Called on |frontend_loop_| to kick off asynchronous authentication.
+ void Authenticate(const std::string& username, const std::string& password);
+
+ // Called on |frontend_loop_| to kick off shutdown.
+ // |sync_disabled| indicates if syncing is being disabled or not.
+ // See the implementation and Core::DoShutdown for details.
+ void Shutdown(bool sync_disabled);
+
+ // Called on |frontend_loop_| to obtain a handle to the UserShare needed
+ // for creating transactions.
+ UserShareHandle GetUserShareHandle() const;
+
+ // Called from any thread to obtain current status information in detailed or
+ // summarized form.
+ Status GetDetailedStatus();
+ StatusSummary GetStatusSummary();
+ AuthErrorState GetAuthErrorState() const;
+
+ const FilePath& sync_data_folder_path() const {
+ return sync_data_folder_path_;
+ }
+
+ // Returns the authenticated username of the sync user, or empty if none
+ // exists. It will only exist if the authentication service provider (e.g
+ // GAIA) has confirmed the username is authentic.
+ string16 GetAuthenticatedUsername() const;
+
+#ifdef UNIT_TEST
+ // Called from unit test to bypass authentication and initialize the syncapi
+ // to a state suitable for testing but not production.
+ void InitializeForTestMode(const std::wstring& test_user) {
+ if (!core_thread_.Start())
+ return;
+ bookmark_model_worker_ = new BookmarkModelWorker(frontend_loop_);
+ core_thread_.message_loop()->PostTask(FROM_HERE,
+ NewRunnableMethod(core_.get(),
+ &SyncBackendHost::Core::DoInitializeForTest,
+ bookmark_model_worker_,
+ test_user));
+ }
+#endif
+
+ private:
+ // The real guts of SyncBackendHost, to keep the public client API clean.
+ class Core : public base::RefCountedThreadSafe<SyncBackendHost::Core>,
+ public sync_api::SyncManager::Observer {
+ public:
+ explicit Core(SyncBackendHost* backend);
+
+ // Note: This destructor should *always* be called from the thread that
+ // created it, and *always* after core_thread_ has exited. The syncapi
+ // watches thread exit events and keeps pointers to objects this dtor will
+ // destroy, so this ordering is important.
+ ~Core() {
+ }
+
+ // SyncManager::Observer implementation. The Core just acts like an air
+ // traffic controller here, forwarding incoming messages to appropriate
+ // landing threads.
+ virtual void OnChangesApplied(
+ const sync_api::BaseTransaction* trans,
+ const sync_api::SyncManager::ChangeRecord* changes,
+ int change_count);
+ virtual void OnSyncCycleCompleted();
+ virtual void OnInitializationComplete();
+ virtual void OnAuthProblem(
+ sync_api::SyncManager::AuthProblem auth_problem);
+
+ // Note:
+ //
+ // The Do* methods are the various entry points from our SyncBackendHost.
+ // It calls us on a dedicated thread to actually perform synchronous
+ // (and potentially blocking) syncapi operations.
+ //
+ // Called on the SyncBackendHost core_thread_ to perform initialization
+ // of the syncapi on behalf of SyncBackendHost::Initialize.
+ void DoInitialize(const GURL& service_url,
+ BookmarkModelWorker* bookmark_model_worker_,
+ bool attempt_last_user_authentication);
+
+ // Called on our SyncBackendHost's core_thread_ to perform authentication
+ // on behalf of SyncBackendHost::Authenticate.
+ void DoAuthenticate(const std::string& username,
+ const std::string& password);
+
+ // The shutdown order is a bit complicated:
+ // 1) From |core_thread_|, invoke the syncapi Shutdown call to do a final
+ // SaveChanges, close sqlite handles, and halt the syncer thread (which
+ // could potentially block for 1 minute).
+ // 2) Then, from |frontend_loop_|, halt the core_thread_. This causes
+ // syncapi thread-exit handlers to run and make use of cached pointers to
+ // various components owned implicitly by us.
+ // 3) Destroy this Core. That will delete syncapi components in a safe order
+ // because the thread that was using them has exited (in step 2).
+ void DoShutdown(bool stopping_sync);
+
+ sync_api::SyncManager* syncapi() { return syncapi_.get(); }
+
+#ifdef UNIT_TEST
+ // Special form of initialization that does not try and authenticate the
+ // last known user (since it will fail in test mode) and does some extra
+ // setup to nudge the syncapi into a useable state.
+ void DoInitializeForTest(BookmarkModelWorker* bookmark_model_worker,
+ const std::wstring& test_user) {
+ DoInitialize(GURL(), bookmark_model_worker, false);
+ syncapi_->SetupForTestMode(WideToUTF16(test_user).c_str());
+ }
+#endif
+
+ private:
+ // FrontendNotification defines parameters for NotifyFrontend. Each enum
+ // value corresponds to the one SyncFrontend interface method that
+ // NotifyFrontend should invoke.
+ enum FrontendNotification {
+ INITIALIZED, // OnBackendInitialized.
+ SYNC_CYCLE_COMPLETED, // A round-trip sync-cycle took place and
+ // the syncer has resolved any conflicts
+ // that may have arisen.
+ };
+
+ // NotifyFrontend is how the Core communicates with the frontend across
+ // threads. Having this extra method (rather than having the Core PostTask
+ // to the frontend explicitly) means SyncFrontend implementations don't
+ // need to be RefCountedThreadSafe because NotifyFrontend is invoked on the
+ // |frontend_loop_|.
+ void NotifyFrontend(FrontendNotification notification);
+
+ // Invoked when initialization of syncapi is complete and we can start
+ // our timer.
+ // This must be called from the thread on which SaveChanges is intended to
+ // be run on; the host's |core_thread_|.
+ void StartSavingChanges();
+
+ // Invoked periodically to tell the syncapi to persist its state
+ // by writing to disk.
+ // This is called from the thread we were created on (which is the
+ // SyncBackendHost |core_thread_|), using a repeating timer that is kicked
+ // off as soon as the SyncManager tells us it completed
+ // initialization.
+ void SaveChanges();
+
+ // Dispatched to from HandleAuthErrorEventOnCoreLoop to handle updating
+ // frontend UI components.
+ void HandleAuthErrorEventOnFrontendLoop(AuthErrorState new_auth_error);
+
+ // Our parent SyncBackendHost
+ SyncBackendHost* host_;
+
+ // The timer used to periodically call SaveChanges.
+ base::RepeatingTimer<Core> save_changes_timer_;
+
+ // The top-level syncapi entry point.
+ scoped_ptr<sync_api::SyncManager> syncapi_;
+
+ DISALLOW_COPY_AND_ASSIGN(Core);
+ };
+
+ // A thread we dedicate for use by our Core to perform initialization,
+ // authentication, handle messages from the syncapi, and periodically tell
+ // the syncapi to persist itself.
+ base::Thread core_thread_;
+
+ // Our core, which communicates directly to the syncapi.
+ scoped_refptr<Core> core_;
+
+ // A reference to the MessageLoop used to construct |this|, so we know how
+ // to safely talk back to the SyncFrontend.
+ MessageLoop* const frontend_loop_;
+
+ // We hold on to the BookmarkModelWorker created for the syncapi to ensure
+ // shutdown occurs in the sequence we expect by calling Stop() at the
+ // appropriate time. It is guaranteed to be valid because the worker is
+ // only destroyed when the SyncManager is destroyed, which happens when
+ // our Core is destroyed, which happens in Shutdown().
+ BookmarkModelWorker* bookmark_model_worker_;
+
+ // The frontend which we serve (and are owned by).
+ SyncFrontend* frontend_;
+
+ // Path of the folder that stores the sync data files.
+ FilePath sync_data_folder_path_;
+
+ // UI-thread cache of the last AuthErrorState received from syncapi.
+ AuthErrorState last_auth_error_;
+
+ DISALLOW_COPY_AND_ASSIGN(SyncBackendHost);
+};
+
+// SyncFrontend is the interface used by SyncBackendHost to communicate with
+// the entity that created it and, presumably, is interested in sync-related
+// activity.
+// NOTE: All methods will be invoked by a SyncBackendHost on the same thread
+// used to create that SyncBackendHost.
+class SyncFrontend {
+ public:
+ typedef sync_api::BaseTransaction BaseTransaction;
+ typedef sync_api::SyncManager::ChangeRecord ChangeRecord;
+ SyncFrontend() {
+ }
+
+ // The backend has completed initialization and it is now ready to accept and
+ // process changes.
+ virtual void OnBackendInitialized() = 0;
+
+ // The backend queried the server recently and received some updates.
+ virtual void OnSyncCycleCompleted() = 0;
+
+ // The backend encountered an authentication problem and requests new
+ // credentials to be provided. See SyncBackendHost::Authenticate for details.
+ virtual void OnAuthError() = 0;
+
+ // Changes have been applied to the backend model and are ready to be
+ // applied to the frontend model. See syncapi.h for detailed instructions on
+ // how to interpret and process |changes|.
+ virtual void ApplyModelChanges(const BaseTransaction* trans,
+ const ChangeRecord* changes,
+ int change_count) = 0;
+ protected:
+ // Don't delete through SyncFrontend interface.
+ virtual ~SyncFrontend() {
+ }
+ private:
+ DISALLOW_COPY_AND_ASSIGN(SyncFrontend);
+};
+
+} // namespace browser_sync
+
+#endif // CHROME_BROWSER_SYNC_GLUE_SYNC_BACKEND_HOST_H_
+#endif // CHROME_PERSONALIZATION