diff options
author | sky@google.com <sky@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-09-09 23:08:13 +0000 |
---|---|---|
committer | sky@google.com <sky@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2008-09-09 23:08:13 +0000 |
commit | 68de8b76186059157dc916c499923d46bedb56b1 (patch) | |
tree | 870704b95740f2cafc144ced59b6bbf3c77c39d1 /chrome/browser/bookmarks | |
parent | d9b168764a9e7ad7dcd2561791041f660a54cdde (diff) | |
download | chromium_src-68de8b76186059157dc916c499923d46bedb56b1.zip chromium_src-68de8b76186059157dc916c499923d46bedb56b1.tar.gz chromium_src-68de8b76186059157dc916c499923d46bedb56b1.tar.bz2 |
Moves bookmark related classes into bookmarks directory. There are no
code changes here (other than converting to COPY_AND_BLAH_BLAH and
updating include guards).
BUG=none
TEST=none
Review URL: http://codereview.chromium.org/1868
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@1944 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/bookmarks')
-rw-r--r-- | chrome/browser/bookmarks/bookmark_bar_model.cc | 694 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_bar_model.h | 435 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_bar_model_unittest.cc | 721 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_codec.cc | 195 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_codec.h | 67 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_drag_data.cc | 118 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_drag_data.h | 81 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_drag_data_unittest.cc | 120 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_storage.cc | 175 | ||||
-rw-r--r-- | chrome/browser/bookmarks/bookmark_storage.h | 97 |
10 files changed, 2703 insertions, 0 deletions
diff --git a/chrome/browser/bookmarks/bookmark_bar_model.cc b/chrome/browser/bookmarks/bookmark_bar_model.cc new file mode 100644 index 0000000..df35fe5 --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_bar_model.cc @@ -0,0 +1,694 @@ +// 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. + +#include "chrome/browser/bookmarks/bookmark_bar_model.h" + +#include "base/gfx/png_decoder.h" +#include "chrome/browser/bookmarks/bookmark_storage.h" +#include "chrome/browser/history/query_parser.h" +#include "chrome/browser/profile.h" +#include "chrome/common/scoped_vector.h" + +#include "generated_resources.h" + +namespace { + +// Functions used for sorting. +bool MoreRecentlyModified(BookmarkBarNode* n1, BookmarkBarNode* n2) { + return n1->date_group_modified() > n2->date_group_modified(); +} + +bool MoreRecentlyAdded(BookmarkBarNode* n1, BookmarkBarNode* n2) { + return n1->date_added() > n2->date_added(); +} + +} // namespace + +// BookmarkBarNode ------------------------------------------------------------ + +namespace { + +// ID for BookmarkBarNodes. +// Various places assume an invalid id if == 0, for that reason we start with 1. +int next_id_ = 1; + +} + +const SkBitmap& BookmarkBarNode::GetFavIcon() { + if (!loaded_favicon_) { + loaded_favicon_ = true; + model_->LoadFavIcon(this); + } + return favicon_; +} + +BookmarkBarNode::BookmarkBarNode(BookmarkBarModel* model, const GURL& url) + : model_(model), + id_(next_id_++), + loaded_favicon_(false), + favicon_load_handle_(0), + url_(url), + type_(!url.is_empty() ? history::StarredEntry::URL : + history::StarredEntry::BOOKMARK_BAR), + date_added_(Time::Now()) { +} + +void BookmarkBarNode::Reset(const history::StarredEntry& entry) { + DCHECK(entry.type != history::StarredEntry::URL || + entry.url == url_); + + favicon_ = SkBitmap(); + type_ = entry.type; + date_added_ = entry.date_added; + date_group_modified_ = entry.date_group_modified; + SetTitle(entry.title); +} + +// BookmarkBarModel ----------------------------------------------------------- + +BookmarkBarModel::BookmarkBarModel(Profile* profile) + : profile_(profile), + loaded_(false), +#pragma warning(suppress: 4355) // Okay to pass "this" here. + root_(this, GURL()), + bookmark_bar_node_(NULL), + other_node_(NULL), + waiting_for_history_load_(false), + loaded_signal_(CreateEvent(NULL, TRUE, FALSE, NULL)) { + // Create the bookmark bar and other bookmarks folders. These always exist. + CreateBookmarkBarNode(); + CreateOtherBookmarksNode(); + + // And add them to the root. + // + // WARNING: order is important here, various places assume bookmark bar then + // other node. + root_.Add(0, bookmark_bar_node_); + root_.Add(1, other_node_); + + if (!profile_) { + // Profile is null during testing. + DoneLoading(); + } +} + +BookmarkBarModel::~BookmarkBarModel() { + if (profile_ && store_.get()) { + NotificationService::current()->RemoveObserver( + this, NOTIFY_FAVICON_CHANGED, Source<Profile>(profile_)); + } + + if (waiting_for_history_load_) { + NotificationService::current()->RemoveObserver( + this, NOTIFY_HISTORY_LOADED, Source<Profile>(profile_)); + } + + FOR_EACH_OBSERVER(BookmarkBarModelObserver, observers_, + BookmarkModelBeingDeleted(this)); + + if (store_) { + // The store maintains a reference back to us. We need to tell it we're gone + // so that it doesn't try and invoke a method back on us again. + store_->BookmarkModelDeleted(); + } +} + +void BookmarkBarModel::Load() { + if (store_.get()) { + // If the store is non-null, it means Load was already invoked. Load should + // only be invoked once. + NOTREACHED(); + return; + } + + // Listen for changes to favicons so that we can update the favicon of the + // node appropriately. + NotificationService::current()->AddObserver( + this, NOTIFY_FAVICON_CHANGED, Source<Profile>(profile_)); + + // Load the bookmarks. BookmarkStorage notifies us when done. + store_ = new BookmarkStorage(profile_, this); + store_->LoadBookmarks(false); +} + +BookmarkBarNode* BookmarkBarModel::GetParentForNewNodes() { + std::vector<BookmarkBarNode*> nodes; + + GetMostRecentlyModifiedGroupNodes(&root_, 1, &nodes); + return nodes.empty() ? bookmark_bar_node_ : nodes[0]; +} + +std::vector<BookmarkBarNode*> BookmarkBarModel::GetMostRecentlyModifiedGroups( + size_t max_count) { + std::vector<BookmarkBarNode*> nodes; + GetMostRecentlyModifiedGroupNodes(&root_, max_count, &nodes); + + if (nodes.size() < max_count) { + // Add the bookmark bar and other nodes if there is space. + if (find(nodes.begin(), nodes.end(), bookmark_bar_node_) == nodes.end()) + nodes.push_back(bookmark_bar_node_); + + if (nodes.size() < max_count && + find(nodes.begin(), nodes.end(), other_node_) == nodes.end()) { + nodes.push_back(other_node_); + } + } + return nodes; +} + +void BookmarkBarModel::GetMostRecentlyAddedEntries( + size_t count, + std::vector<BookmarkBarNode*>* nodes) { + AutoLock url_lock(url_lock_); + for (NodesOrderedByURLSet::iterator i = nodes_ordered_by_url_set_.begin(); + i != nodes_ordered_by_url_set_.end(); ++i) { + std::vector<BookmarkBarNode*>::iterator insert_position = + std::upper_bound(nodes->begin(), nodes->end(), *i, &MoreRecentlyAdded); + if (nodes->size() < count || insert_position != nodes->end()) { + nodes->insert(insert_position, *i); + while (nodes->size() > count) + nodes->pop_back(); + } + } +} + +void BookmarkBarModel::GetBookmarksMatchingText( + const std::wstring& text, + size_t max_count, + std::vector<TitleMatch>* matches) { + QueryParser parser; + ScopedVector<QueryNode> query_nodes; + parser.ParseQuery(text, &query_nodes.get()); + if (query_nodes.empty()) + return; + + AutoLock url_lock(url_lock_); + Snippet::MatchPositions match_position; + for (NodesOrderedByURLSet::iterator i = nodes_ordered_by_url_set_.begin(); + i != nodes_ordered_by_url_set_.end(); ++i) { + if (parser.DoesQueryMatch((*i)->GetTitle(), query_nodes.get(), + &match_position)) { + matches->push_back(TitleMatch()); + matches->back().node = *i; + matches->back().match_positions.swap(match_position); + if (matches->size() == max_count) + break; + } + } +} + +void BookmarkBarModel::Remove(BookmarkBarNode* parent, int index) { + if (!loaded_ || !IsValidIndex(parent, index, false) || parent == &root_) { + NOTREACHED(); + return; + } + RemoveAndDeleteNode(parent->GetChild(index)); +} + +void BookmarkBarModel::Move(BookmarkBarNode* node, + BookmarkBarNode* new_parent, + int index) { + if (!loaded_ || !node || !IsValidIndex(new_parent, index, true) || + new_parent == &root_ || node == &root_ || node == bookmark_bar_node_ || + node == other_node_) { + NOTREACHED(); + return; + } + + if (new_parent->HasAncestor(node)) { + // Can't make an ancestor of the node be a child of the node. + NOTREACHED(); + return; + } + + SetDateGroupModified(new_parent, Time::Now()); + + BookmarkBarNode* old_parent = node->GetParent(); + int old_index = old_parent->IndexOfChild(node); + + if (old_parent == new_parent && + (index == old_index || index == old_index + 1)) { + // Node is already in this position, nothing to do. + return; + } + + if (old_parent == new_parent && index > old_index) + index--; + new_parent->Add(index, node); + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkBarModelObserver, observers_, + BookmarkNodeMoved(this, old_parent, old_index, + new_parent, index)); +} + +void BookmarkBarModel::SetTitle(BookmarkBarNode* node, + const std::wstring& title) { + if (!node) { + NOTREACHED(); + return; + } + if (node->GetTitle() == title) + return; + + node->SetTitle(title); + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkBarModelObserver, observers_, + BookmarkNodeChanged(this, node)); +} + +BookmarkBarNode* BookmarkBarModel::GetNodeByURL(const GURL& url) { + AutoLock url_lock(url_lock_); + BookmarkBarNode tmp_node(this, url); + NodesOrderedByURLSet::iterator i = nodes_ordered_by_url_set_.find(&tmp_node); + return (i != nodes_ordered_by_url_set_.end()) ? *i : NULL; +} + +void BookmarkBarModel::GetBookmarks(std::vector<GURL>* urls) { + AutoLock url_lock(url_lock_); + for (NodesOrderedByURLSet::iterator i = nodes_ordered_by_url_set_.begin(); + i != nodes_ordered_by_url_set_.end(); ++i) { + urls->push_back((*i)->GetURL()); + } +} + +BookmarkBarNode* BookmarkBarModel::GetNodeByID(int id) { + // TODO(sky): TreeNode needs a method that visits all nodes using a predicate. + return GetNodeByID(&root_, id); +} + +BookmarkBarNode* BookmarkBarModel::AddGroup( + BookmarkBarNode* parent, + int index, + const std::wstring& title) { + if (!loaded_ || parent == &root_ || !IsValidIndex(parent, index, true)) { + // Can't add to the root. + NOTREACHED(); + return NULL; + } + + BookmarkBarNode* new_node = new BookmarkBarNode(this, GURL()); + new_node->SetTitle(title); + new_node->type_ = history::StarredEntry::USER_GROUP; + + return AddNode(parent, index, new_node); +} + +BookmarkBarNode* BookmarkBarModel::AddURL(BookmarkBarNode* parent, + int index, + const std::wstring& title, + const GURL& url) { + return AddURLWithCreationTime(parent, index, title, url, Time::Now()); +} + +BookmarkBarNode* BookmarkBarModel::AddURLWithCreationTime( + BookmarkBarNode* parent, + int index, + const std::wstring& title, + const GURL& url, + const Time& creation_time) { + if (!loaded_ || !url.is_valid() || parent == &root_ || + !IsValidIndex(parent, index, true)) { + NOTREACHED(); + return NULL; + } + + BookmarkBarNode* existing_node = GetNodeByURL(url); + if (existing_node) { + Move(existing_node, parent, index); + SetTitle(existing_node, title); + return existing_node; + } + + SetDateGroupModified(parent, creation_time); + + BookmarkBarNode* new_node = new BookmarkBarNode(this, url); + new_node->SetTitle(title); + new_node->date_added_ = creation_time; + new_node->type_ = history::StarredEntry::URL; + + AutoLock url_lock(url_lock_); + nodes_ordered_by_url_set_.insert(new_node); + + return AddNode(parent, index, new_node); +} + +void BookmarkBarModel::SetURLStarred(const GURL& url, + const std::wstring& title, + bool is_starred) { + BookmarkBarNode* node = GetNodeByURL(url); + if (is_starred && !node) { + // Add the url. + BookmarkBarNode* parent = GetParentForNewNodes(); + AddURL(parent, parent->GetChildCount(), title, url); + } else if (!is_starred && node) { + Remove(node->GetParent(), node->GetParent()->IndexOfChild(node)); + } +} + +void BookmarkBarModel::ResetDateGroupModified(BookmarkBarNode* node) { + SetDateGroupModified(node, Time()); +} + +void BookmarkBarModel::FavIconLoaded(BookmarkBarNode* node) { + // Send out notification to the observer. + FOR_EACH_OBSERVER(BookmarkBarModelObserver, observers_, + BookmarkNodeFavIconLoaded(this, node)); +} + +void BookmarkBarModel::RemoveNode(BookmarkBarNode* node, + std::set<GURL>* removed_urls) { + if (!loaded_ || !node || node == &root_ || node == bookmark_bar_node_ || + node == other_node_) { + NOTREACHED(); + return; + } + + if (node->GetType() == history::StarredEntry::URL) { + // NOTE: this is called in such a way that url_lock_ is already held. As + // such, this doesn't explicitly grab the lock. + NodesOrderedByURLSet::iterator i = nodes_ordered_by_url_set_.find(node); + DCHECK(i != nodes_ordered_by_url_set_.end()); + nodes_ordered_by_url_set_.erase(i); + removed_urls->insert(node->GetURL()); + } + + CancelPendingFavIconLoadRequests(node); + + // Recurse through children. + for (int i = node->GetChildCount() - 1; i >= 0; --i) + RemoveNode(node->GetChild(i), removed_urls); +} + +void BookmarkBarModel::OnBookmarkStorageLoadedBookmarks( + bool file_exists, + bool loaded_from_history) { + if (loaded_) { + NOTREACHED(); + return; + } + + if (file_exists || loaded_from_history || !profile_ || + !profile_->GetHistoryService(Profile::EXPLICIT_ACCESS)) { + // The file exists, we're loaded. + DoneLoading(); + + if (loaded_from_history) { + // We were just populated from the historical file. Schedule a save so + // that the main file is up to date. + store_->ScheduleSave(); + } + return; + } + + // The file doesn't exist. This means one of two things: + // 1. A clean profile. + // 2. The user is migrating from an older version where bookmarks were saved + // in history. + // We assume step 2. If history had the bookmarks, history will write the + // bookmarks to a file for us. We need to wait until history has finished + // loading before reading from that file. + HistoryService* history = + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (!history->backend_loaded()) { + // The backend isn't finished loading. Wait for it. + waiting_for_history_load_ = true; + NotificationService::current()->AddObserver( + this, NOTIFY_HISTORY_LOADED, Source<Profile>(profile_)); + } else { + OnHistoryDone(); + } +} + +void BookmarkBarModel::OnHistoryDone() { + if (loaded_) { + NOTREACHED(); + return; + } + + // If the bookmarks were stored in the db the db will have migrated them to + // a file now. Try loading from the file. + store_->LoadBookmarks(true); +} + +void BookmarkBarModel::DoneLoading() { + { + AutoLock url_lock(url_lock_); + // Update nodes_ordered_by_url_set_ from the nodes. + PopulateNodesByURL(&root_); + } + + loaded_ = true; + + if (loaded_signal_.Get()) + SetEvent(loaded_signal_.Get()); + + + // Notify our direct observers. + FOR_EACH_OBSERVER(BookmarkBarModelObserver, observers_, Loaded(this)); + + // And generic notification. + NotificationService::current()->Notify( + NOTIFY_BOOKMARK_MODEL_LOADED, + Source<Profile>(profile_), + NotificationService::NoDetails()); +} + +void BookmarkBarModel::RemoveAndDeleteNode(BookmarkBarNode* delete_me) { + scoped_ptr<BookmarkBarNode> node(delete_me); + + BookmarkBarNode* parent = node->GetParent(); + DCHECK(parent); + int index = parent->IndexOfChild(node.get()); + parent->Remove(index); + history::URLsStarredDetails details(false); + { + AutoLock url_lock(url_lock_); + RemoveNode(node.get(), &details.changed_urls); + } + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkBarModelObserver, observers_, + BookmarkNodeRemoved(this, parent, index)); + + if (profile_) { + HistoryService* history = + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (history) + history->URLsNoLongerBookmarked(details.changed_urls); + } + + NotificationService::current()->Notify(NOTIFY_URLS_STARRED, + Source<Profile>(profile_), + Details<history::URLsStarredDetails>(&details)); +} + +BookmarkBarNode* BookmarkBarModel::AddNode(BookmarkBarNode* parent, + int index, + BookmarkBarNode* node) { + parent->Add(index, node); + + if (store_.get()) + store_->ScheduleSave(); + + FOR_EACH_OBSERVER(BookmarkBarModelObserver, observers_, + BookmarkNodeAdded(this, parent, index)); + + if (node->GetType() == history::StarredEntry::URL) { + history::URLsStarredDetails details(true); + details.changed_urls.insert(node->GetURL()); + NotificationService::current()->Notify(NOTIFY_URLS_STARRED, + Source<Profile>(profile_), + Details<history::URLsStarredDetails>(&details)); + } + return node; +} + +void BookmarkBarModel::BlockTillLoaded() { + if (loaded_signal_.Get()) + WaitForSingleObject(loaded_signal_.Get(), INFINITE); +} + +BookmarkBarNode* BookmarkBarModel::GetNodeByID(BookmarkBarNode* node, + int id) { + if (node->id() == id) + return node; + + for (int i = 0; i < node->GetChildCount(); ++i) { + BookmarkBarNode* result = GetNodeByID(node->GetChild(i), id); + if (result) + return result; + } + return NULL; +} + +bool BookmarkBarModel::IsValidIndex(BookmarkBarNode* parent, + int index, + bool allow_end) { + return (parent && + (index >= 0 && (index < parent->GetChildCount() || + (allow_end && index == parent->GetChildCount())))); + } + +void BookmarkBarModel::SetDateGroupModified(BookmarkBarNode* parent, + const Time time) { + DCHECK(parent); + parent->date_group_modified_ = time; + + if (store_.get()) + store_->ScheduleSave(); +} + +void BookmarkBarModel::CreateBookmarkBarNode() { + history::StarredEntry entry; + entry.type = history::StarredEntry::BOOKMARK_BAR; + bookmark_bar_node_ = CreateRootNodeFromStarredEntry(entry); +} + +void BookmarkBarModel::CreateOtherBookmarksNode() { + history::StarredEntry entry; + entry.type = history::StarredEntry::OTHER; + other_node_ = CreateRootNodeFromStarredEntry(entry); +} + +BookmarkBarNode* BookmarkBarModel::CreateRootNodeFromStarredEntry( + const history::StarredEntry& entry) { + DCHECK(entry.type == history::StarredEntry::BOOKMARK_BAR || + entry.type == history::StarredEntry::OTHER); + BookmarkBarNode* node = new BookmarkBarNode(this, GURL()); + node->Reset(entry); + if (entry.type == history::StarredEntry::BOOKMARK_BAR) + node->SetTitle(l10n_util::GetString(IDS_BOOMARK_BAR_FOLDER_NAME)); + else + node->SetTitle(l10n_util::GetString(IDS_BOOMARK_BAR_OTHER_FOLDER_NAME)); + return node; +} + +void BookmarkBarModel::OnFavIconDataAvailable( + HistoryService::Handle handle, + bool know_favicon, + scoped_refptr<RefCountedBytes> data, + bool expired, + GURL icon_url) { + SkBitmap fav_icon; + BookmarkBarNode* node = + load_consumer_.GetClientData( + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS), handle); + DCHECK(node); + node->favicon_load_handle_ = 0; + if (know_favicon && data.get() && + PNGDecoder::Decode(&data->data, &fav_icon)) { + node->favicon_ = fav_icon; + FavIconLoaded(node); + } +} + +void BookmarkBarModel::LoadFavIcon(BookmarkBarNode* node) { + if (node->GetType() != history::StarredEntry::URL) + return; + + DCHECK(node->GetURL().is_valid()); + HistoryService* history_service = + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (!history_service) + return; + + HistoryService::Handle handle = history_service->GetFavIconForURL( + node->GetURL(), &load_consumer_, + NewCallback(this, &BookmarkBarModel::OnFavIconDataAvailable)); + load_consumer_.SetClientData(history_service, handle, node); + node->favicon_load_handle_ = handle; +} + +void BookmarkBarModel::CancelPendingFavIconLoadRequests(BookmarkBarNode* node) { + if (node->favicon_load_handle_) { + HistoryService* history = + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); + if (history) + history->CancelRequest(node->favicon_load_handle_); + node->favicon_load_handle_ = 0; + } +} + +void BookmarkBarModel::GetMostRecentlyModifiedGroupNodes( + BookmarkBarNode* parent, + size_t count, + std::vector<BookmarkBarNode*>* nodes) { + if (parent != &root_ && parent->is_folder() && + parent->date_group_modified() > Time()) { + if (count == 0) { + nodes->push_back(parent); + } else { + std::vector<BookmarkBarNode*>::iterator i = + std::upper_bound(nodes->begin(), nodes->end(), parent, + &MoreRecentlyModified); + if (nodes->size() < count || i != nodes->end()) { + nodes->insert(i, parent); + while (nodes->size() > count) + nodes->pop_back(); + } + } + } // else case, the root node, which we don't care about or imported nodes + // (which have a time of 0). + for (int i = 0; i < parent->GetChildCount(); ++i) { + BookmarkBarNode* child = parent->GetChild(i); + if (child->is_folder()) + GetMostRecentlyModifiedGroupNodes(child, count, nodes); + } +} + +void BookmarkBarModel::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + switch (type) { + case NOTIFY_FAVICON_CHANGED: { + // Prevent the observers from getting confused for multiple favicon loads. + Details<history::FavIconChangeDetails> favicon_details(details); + for (std::set<GURL>::const_iterator i = favicon_details->urls.begin(); + i != favicon_details->urls.end(); ++i) { + BookmarkBarNode* node = GetNodeByURL(*i); + if (node) { + // Got an updated favicon, for a URL, do a new request. + node->InvalidateFavicon(); + CancelPendingFavIconLoadRequests(node); + FOR_EACH_OBSERVER(BookmarkBarModelObserver, observers_, + BookmarkNodeChanged(this, node)); + } + } + break; + } + + case NOTIFY_HISTORY_LOADED: { + if (waiting_for_history_load_) { + waiting_for_history_load_ = false; + NotificationService::current()->RemoveObserver( + this, NOTIFY_HISTORY_LOADED, Source<Profile>(profile_)); + OnHistoryDone(); + } else { + NOTREACHED(); + } + break; + } + + default: + NOTREACHED(); + break; + } +} + +void BookmarkBarModel::PopulateNodesByURL(BookmarkBarNode* node) { + // NOTE: this is called with url_lock_ already held. As such, this doesn't + // explicitly grab the lock. + if (node->is_url()) + nodes_ordered_by_url_set_.insert(node); + for (int i = 0; i < node->GetChildCount(); ++i) + PopulateNodesByURL(node->GetChild(i)); +} diff --git a/chrome/browser/bookmarks/bookmark_bar_model.h b/chrome/browser/bookmarks/bookmark_bar_model.h new file mode 100644 index 0000000..25c19ad --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_bar_model.h @@ -0,0 +1,435 @@ +// 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. + +#ifndef CHROME_BROWSER_BOOKMARKS_BOOKMARK_BAR_MODEL_H_ +#define CHROME_BROWSER_BOOKMARKS_BOOKMARK_BAR_MODEL_H_ + +#include "base/lock.h" +#include "base/observer_list.h" +#include "base/scoped_handle.h" +#include "chrome/browser/bookmarks/bookmark_service.h" +#include "chrome/browser/bookmarks/bookmark_storage.h" +#include "chrome/browser/cancelable_request.h" +#include "chrome/browser/history/history.h" +#include "chrome/browser/history/history_types.h" +#include "chrome/common/notification_service.h" +#include "chrome/views/tree_node_model.h" +#include "googleurl/src/gurl.h" +#include "skia/include/SkBitmap.h" + +class BookmarkBarModel; +class BookmarkCodec; +class Profile; + +namespace history { +class StarredURLDatabase; +} + +// BookmarkBarNode ------------------------------------------------------------ + +// BookmarkBarNode contains information about a starred entry: title, URL, +// favicon, star id and type. BookmarkBarNodes are returned from a +// BookmarkBarModel. +// +class BookmarkBarNode : public ChromeViews::TreeNode<BookmarkBarNode> { + friend class BookmarkBarModel; + friend class BookmarkCodec; + friend class history::StarredURLDatabase; + FRIEND_TEST(BookmarkBarModelTest, MostRecentlyAddedEntries); + + public: + BookmarkBarNode(BookmarkBarModel* model, const GURL& url); + virtual ~BookmarkBarNode() {} + + // Returns the favicon for the this node. If the favicon has not yet been + // loaded it is loaded and the observer of the model notified when done. + const SkBitmap& GetFavIcon(); + + // Returns the URL. + const GURL& GetURL() const { return url_; } + + // Returns a unique id for this node. + // + // NOTE: this id is only unique for the session and NOT unique across + // sessions. Don't persist it! + int id() const { return id_; } + + // Returns the type of this node. + history::StarredEntry::Type GetType() const { return type_; } + + // Called when the favicon becomes invalid. + void InvalidateFavicon() { + loaded_favicon_ = false; + favicon_ = SkBitmap(); + } + + // Returns the time the bookmark/group was added. + Time date_added() const { return date_added_; } + + // Returns the last time the group was modified. This is only maintained + // for folders (including the bookmark and other folder). + Time date_group_modified() const { return date_group_modified_; } + + // Convenience for testing if this nodes represents a group. A group is + // a node whose type is not URL. + bool is_folder() const { return type_ != history::StarredEntry::URL; } + + // Is this a URL? + bool is_url() const { return type_ == history::StarredEntry::URL; } + + // TODO(sky): Consider adding last visit time here, it'll greatly simplify + // HistoryContentsProvider. + + private: + // Resets the properties of the node from the supplied entry. + void Reset(const history::StarredEntry& entry); + + // The model. This is NULL when created by StarredURLDatabase for migration. + BookmarkBarModel* model_; + + // Unique identifier for this node. + const int id_; + + // Whether the favicon has been loaded. + bool loaded_favicon_; + + // The favicon. + SkBitmap favicon_; + + // If non-zero, it indicates we're loading the favicon and this is the handle + // from the HistoryService. + HistoryService::Handle favicon_load_handle_; + + // The URL. BookmarkBarModel maintains maps off this URL, it is important that + // it not change once the node has been created. + const GURL url_; + + // Type of node. + // TODO(sky): bug 1256202, convert this into a type defined here. + history::StarredEntry::Type type_; + + // Date we were created. + Time date_added_; + + // Time last modified. Only used for groups. + Time date_group_modified_; + + DISALLOW_EVIL_CONSTRUCTORS(BookmarkBarNode); +}; + + +// BookmarkBarModelObserver --------------------------------------------------- + +// Observer for the BookmarkBarModel. +// +class BookmarkBarModelObserver { + public: + // Invoked when the model has finished loading. + virtual void Loaded(BookmarkBarModel* model) = 0; + + // Invoked from the destructor of the BookmarkBarModel. + virtual void BookmarkModelBeingDeleted(BookmarkBarModel* model) { } + + // Invoked when a node has moved. + virtual void BookmarkNodeMoved(BookmarkBarModel* model, + BookmarkBarNode* old_parent, + int old_index, + BookmarkBarNode* new_parent, + int new_index) = 0; + + // Invoked when a node has been added. + virtual void BookmarkNodeAdded(BookmarkBarModel* model, + BookmarkBarNode* parent, + int index) = 0; + + // Invoked when a node has been removed, the item may still be starred though. + virtual void BookmarkNodeRemoved(BookmarkBarModel* model, + BookmarkBarNode* parent, + int index) = 0; + + // Invoked when the title or favicon of a node has changed. + virtual void BookmarkNodeChanged(BookmarkBarModel* model, + BookmarkBarNode* node) = 0; + + // Invoked when a favicon has finished loading. + virtual void BookmarkNodeFavIconLoaded(BookmarkBarModel* model, + BookmarkBarNode* node) = 0; +}; + +// BookmarkBarModel ----------------------------------------------------------- + +// BookmarkBarModel provides a directed acyclic graph of the starred entries +// and groups. Two graphs are provided for the two entry points: those on +// the bookmark bar, and those in the other folder. +// +// The methods of BookmarkBarModel update the internal structure immediately +// and update the backend in the background. +// +// An observer may be attached to observer relevant events. +// +// You should NOT directly create a BookmarkBarModel, instead go through the +// Profile. + +// TODO(sky): rename to BookmarkModel. +class BookmarkBarModel : public NotificationObserver, public BookmarkService { + friend class BookmarkBarNode; + friend class BookmarkBarModelTest; + friend class BookmarkStorage; + + public: + explicit BookmarkBarModel(Profile* profile); + virtual ~BookmarkBarModel(); + + // Loads the bookmarks. This is called by Profile upon creation of the + // BookmarkBarModel. You need not invoke this directly. + void Load(); + + // Returns the root node. The bookmark bar node and other node are children of + // the root node. + BookmarkBarNode* root_node() { return &root_; } + + // Returns the bookmark bar node. + BookmarkBarNode* GetBookmarkBarNode() { return bookmark_bar_node_; } + + // Returns the 'other' node. + BookmarkBarNode* other_node() { return other_node_; } + + // Returns the parent the last node was added to. This never returns NULL + // (as long as the model is loaded). + BookmarkBarNode* GetParentForNewNodes(); + + // Returns a vector containing up to |max_count| of the most recently + // modified groups. This never returns an empty vector. + std::vector<BookmarkBarNode*> GetMostRecentlyModifiedGroups(size_t max_count); + + // Returns the most recently added bookmarks. + void GetMostRecentlyAddedEntries(size_t count, + std::vector<BookmarkBarNode*>* nodes); + + // Used by GetBookmarksMatchingText to return a matching node and the location + // of the match in the title. + struct TitleMatch { + BookmarkBarNode* node; + + // Location of the matching words in the title of the node. + Snippet::MatchPositions match_positions; + }; + + // Returns the bookmarks whose title contains text. At most |max_count| + // matches are returned in |matches|. + void GetBookmarksMatchingText(const std::wstring& text, + size_t max_count, + std::vector<TitleMatch>* matches); + + void AddObserver(BookmarkBarModelObserver* observer) { + observers_.AddObserver(observer); + } + + void RemoveObserver(BookmarkBarModelObserver* observer) { + observers_.RemoveObserver(observer); + } + + // Unstars or deletes the specified entry. Removing a group entry recursively + // unstars all nodes. Observers are notified immediately. + void Remove(BookmarkBarNode* parent, int index); + + // Moves the specified entry to a new location. + void Move(BookmarkBarNode* node, BookmarkBarNode* new_parent, int index); + + // Sets the title of the specified node. + void SetTitle(BookmarkBarNode* node, const std::wstring& title); + + // Returns true if the model finished loading. + bool IsLoaded() { return loaded_; } + + // Returns the node with the specified URL, or NULL if there is no node with + // the specified URL. This method is thread safe. + BookmarkBarNode* GetNodeByURL(const GURL& url); + + // Returns all the bookmarked urls. This method is thread safe. + virtual void GetBookmarks(std::vector<GURL>* urls); + + // Returns true if there is a bookmark for the specified URL. This method is + // thread safe. See BookmarkService for more details on this. + virtual bool IsBookmarked(const GURL& url) { + return GetNodeByURL(url) != NULL; + } + + // Blocks until loaded; this is NOT invoked on the main thread. See + // BookmarkService for more details on this. + virtual void BlockTillLoaded(); + + // Returns the node with the specified id, or NULL if there is no node with + // the specified id. + BookmarkBarNode* GetNodeByID(int id); + + // Adds a new group node at the specified position. + BookmarkBarNode* AddGroup(BookmarkBarNode* parent, + int index, + const std::wstring& title); + + // Adds a url at the specified position. If there is already a node with the + // specified URL, it is moved to the new position. + BookmarkBarNode* AddURL(BookmarkBarNode* parent, + int index, + const std::wstring& title, + const GURL& url); + + // Adds a url with a specific creation date. + BookmarkBarNode* AddURLWithCreationTime(BookmarkBarNode* parent, + int index, + const std::wstring& title, + const GURL& url, + const Time& creation_time); + + // This is the convenience that makes sure the url is starred or not + // starred. If the URL is not currently starred, it is added to the + // most recent parent. + void SetURLStarred(const GURL& url, + const std::wstring& title, + bool is_starred); + + // Resets the 'date modified' time of the node to 0. This is used during + // importing to exclude the newly created groups from showing up in the + // combobox of most recently modified groups. + void ResetDateGroupModified(BookmarkBarNode* node); + + private: + // Used to order BookmarkBarNodes by URL. + class NodeURLComparator { + public: + bool operator()(BookmarkBarNode* n1, BookmarkBarNode* n2) const { + return n1->GetURL() < n2->GetURL(); + } + }; + + // Overriden to notify the observer the favicon has been loaded. + void FavIconLoaded(BookmarkBarNode* node); + + // Removes the node from internal maps and recurces through all children. If + // the node is a url, its url is added to removed_urls. + // + // This does NOT delete the node. + void RemoveNode(BookmarkBarNode* node, std::set<GURL>* removed_urls); + + // Callback from BookmarkStorage that it has finished loading. This method + // may be hit twice. In particular, on construction BookmarkBarModel asks + // BookmarkStorage to load the bookmarks. BookmarkStorage invokes this method + // with loaded_from_history false and file_exists indicating whether the + // bookmarks file exists. If the file doesn't exist, we query history. When + // history calls us back (OnHistoryDone) we then ask BookmarkStorage to load + // from the migration file. BookmarkStorage again invokes this method, but + // with |loaded_from_history| true. + void OnBookmarkStorageLoadedBookmarks(bool file_exists, + bool loaded_from_history); + + // Used for migrating bookmarks from history to standalone file. + // + // Callback from history that it is done with an empty request. This is used + // if there is no bookmarks file. Once done, we attempt to load from the + // temporary file creating during migration. + void OnHistoryDone(); + + // Invoked when loading is finished. Sets loaded_ and notifies observers. + void DoneLoading(); + + // Populates nodes_ordered_by_url_set_ from root. + void PopulateNodesByURL(BookmarkBarNode* node); + + // Removes the node from its parent, sends notification, and deletes it. + // type specifies how the node should be removed. + void RemoveAndDeleteNode(BookmarkBarNode* delete_me); + + // Adds the node at the specified position, and sends notification. + BookmarkBarNode* AddNode(BookmarkBarNode* parent, + int index, + BookmarkBarNode* node); + + // Implementation of GetNodeByID. + BookmarkBarNode* GetNodeByID(BookmarkBarNode* node, int id); + + // Returns true if the parent and index are valid. + bool IsValidIndex(BookmarkBarNode* parent, int index, bool allow_end); + + // Sets the date modified time of the specified node. + void SetDateGroupModified(BookmarkBarNode* parent, const Time time); + + // Creates the bookmark bar/other nodes. These call into + // CreateRootNodeFromStarredEntry. + void CreateBookmarkBarNode(); + void CreateOtherBookmarksNode(); + + // Creates a root node (either the bookmark bar node or other node) from the + // specified starred entry. + BookmarkBarNode* CreateRootNodeFromStarredEntry( + const history::StarredEntry& entry); + + // Notification that a favicon has finished loading. If we can decode the + // favicon, FaviconLoaded is invoked. + void OnFavIconDataAvailable( + HistoryService::Handle handle, + bool know_favicon, + scoped_refptr<RefCountedBytes> data, + bool expired, + GURL icon_url); + + // Invoked from the node to load the favicon. Requests the favicon from the + // history service. + void LoadFavIcon(BookmarkBarNode* node); + + // If we're waiting on a favicon for node, the load request is canceled. + void CancelPendingFavIconLoadRequests(BookmarkBarNode* node); + + // Returns up to count of the most recently modified groups. This may not + // add anything. + void GetMostRecentlyModifiedGroupNodes(BookmarkBarNode* parent, + size_t count, + std::vector<BookmarkBarNode*>* nodes); + + // NotificationObserver. + virtual void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + + Profile* profile_; + + // Whether the initial set of data has been loaded. + bool loaded_; + + // The root node. This contains the bookmark bar node and the 'other' node as + // children. + BookmarkBarNode root_; + + BookmarkBarNode* bookmark_bar_node_; + BookmarkBarNode* other_node_; + + // The observers. + ObserverList<BookmarkBarModelObserver> observers_; + + // Set of nodes ordered by URL. This is not a map to avoid copying the + // urls. + // WARNING: nodes_ordered_by_url_set_ is accessed on multiple threads. As + // such, be sure and wrap all usage of it around url_lock_. + typedef std::set<BookmarkBarNode*,NodeURLComparator> NodesOrderedByURLSet; + NodesOrderedByURLSet nodes_ordered_by_url_set_; + Lock url_lock_; + + // Used for loading favicons and the empty history request. + CancelableRequestConsumerT<BookmarkBarNode*, NULL> load_consumer_; + + // Reads/writes bookmarks to disk. + scoped_refptr<BookmarkStorage> store_; + + // Have we installed a listener on the NotificationService for + // NOTIFY_HISTORY_LOADED? A listener is installed if the bookmarks file + // doesn't exist and the history service hasn't finished loading. + bool waiting_for_history_load_; + + // Handle to event signaled when loading is done. + ScopedHandle loaded_signal_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkBarModel); +}; + +#endif // CHROME_BROWSER_BOOKMARKS_BOOKMARK_BAR_MODEL_H_ diff --git a/chrome/browser/bookmarks/bookmark_bar_model_unittest.cc b/chrome/browser/bookmarks/bookmark_bar_model_unittest.cc new file mode 100644 index 0000000..f6d4b2d --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_bar_model_unittest.cc @@ -0,0 +1,721 @@ +// 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. + +#include "base/string_util.h" +#include "chrome/browser/bookmarks/bookmark_bar_model.h" +#include "chrome/browser/bookmarks/bookmark_codec.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/test/testing_profile.h" +#include "chrome/views/tree_node_model.h" +#include "testing/gtest/include/gtest/gtest.h" + +class BookmarkBarModelTest : public testing::Test, + public BookmarkBarModelObserver { + public: + struct ObserverDetails { + ObserverDetails() { + Set(NULL, NULL, -1, -1); + } + + void Set(BookmarkBarNode* node1, + BookmarkBarNode* node2, + int index1, + int index2) { + this->node1 = node1; + this->node2 = node2; + this->index1 = index1; + this->index2 = index2; + } + + void AssertEquals(BookmarkBarNode* node1, + BookmarkBarNode* node2, + int index1, + int index2) { + ASSERT_TRUE(this->node1 == node1); + ASSERT_TRUE(this->node2 == node2); + ASSERT_EQ(index1, this->index1); + ASSERT_EQ(index2, this->index2); + } + + BookmarkBarNode* node1; + BookmarkBarNode* node2; + int index1; + int index2; + }; + + BookmarkBarModelTest() : model(NULL) { + model.AddObserver(this); + ClearCounts(); + } + + + void Loaded(BookmarkBarModel* model) { + // We never load from the db, so that this should never get invoked. + NOTREACHED(); + } + + virtual void BookmarkNodeMoved(BookmarkBarModel* model, + BookmarkBarNode* old_parent, + int old_index, + BookmarkBarNode* new_parent, + int new_index) { + moved_count++; + observer_details.Set(old_parent, new_parent, old_index, new_index); + } + + virtual void BookmarkNodeAdded(BookmarkBarModel* model, + BookmarkBarNode* parent, + int index) { + added_count++; + observer_details.Set(parent, NULL, index, -1); + } + + virtual void BookmarkNodeRemoved(BookmarkBarModel* model, + BookmarkBarNode* parent, + int index) { + removed_count++; + observer_details.Set(parent, NULL, index, -1); + } + + virtual void BookmarkNodeChanged(BookmarkBarModel* model, + BookmarkBarNode* node) { + changed_count++; + observer_details.Set(node, NULL, -1, -1); + } + + virtual void BookmarkNodeFavIconLoaded(BookmarkBarModel* model, + BookmarkBarNode* node) { + // We never attempt to load favicons, so that this method never + // gets invoked. + } + + void ClearCounts() { + moved_count = added_count = removed_count = changed_count = 0; + } + + void AssertObserverCount(int added_count, + int moved_count, + int removed_count, + int changed_count) { + ASSERT_EQ(added_count, this->added_count); + ASSERT_EQ(moved_count, this->moved_count); + ASSERT_EQ(removed_count, this->removed_count); + ASSERT_EQ(changed_count, this->changed_count); + } + + void AssertNodesEqual(BookmarkBarNode* expected, BookmarkBarNode* actual) { + ASSERT_TRUE(expected); + ASSERT_TRUE(actual); + EXPECT_EQ(expected->GetTitle(), actual->GetTitle()); + EXPECT_EQ(expected->GetType(), actual->GetType()); + EXPECT_TRUE(expected->date_added() == actual->date_added()); + if (expected->GetType() == history::StarredEntry::URL) { + EXPECT_EQ(expected->GetURL(), actual->GetURL()); + } else { + EXPECT_TRUE(expected->date_group_modified() == + actual->date_group_modified()); + ASSERT_EQ(expected->GetChildCount(), actual->GetChildCount()); + for (int i = 0; i < expected->GetChildCount(); ++i) + AssertNodesEqual(expected->GetChild(i), actual->GetChild(i)); + } + } + + void AssertModelsEqual(BookmarkBarModel* expected, + BookmarkBarModel* actual) { + AssertNodesEqual(expected->GetBookmarkBarNode(), + actual->GetBookmarkBarNode()); + AssertNodesEqual(expected->other_node(), + actual->other_node()); + } + + BookmarkBarModel model; + + int moved_count; + + int added_count; + + int removed_count; + + int changed_count; + + ObserverDetails observer_details; +}; + +TEST_F(BookmarkBarModelTest, InitialState) { + BookmarkBarNode* bb_node = model.GetBookmarkBarNode(); + ASSERT_TRUE(bb_node != NULL); + EXPECT_EQ(0, bb_node->GetChildCount()); + EXPECT_EQ(history::StarredEntry::BOOKMARK_BAR, bb_node->GetType()); + + BookmarkBarNode* other_node = model.other_node(); + ASSERT_TRUE(other_node != NULL); + EXPECT_EQ(0, other_node->GetChildCount()); + EXPECT_EQ(history::StarredEntry::OTHER, other_node->GetType()); + + EXPECT_TRUE(bb_node->id() != other_node->id()); +} + +TEST_F(BookmarkBarModelTest, AddURL) { + BookmarkBarNode* root = model.GetBookmarkBarNode(); + const std::wstring title(L"foo"); + const GURL url("http://foo.com"); + + BookmarkBarNode* new_node = model.AddURL(root, 0, title, url); + AssertObserverCount(1, 0, 0, 0); + observer_details.AssertEquals(root, NULL, 0, -1); + + ASSERT_EQ(1, root->GetChildCount()); + ASSERT_EQ(title, new_node->GetTitle()); + ASSERT_TRUE(url == new_node->GetURL()); + ASSERT_EQ(history::StarredEntry::URL, new_node->GetType()); + ASSERT_TRUE(new_node == model.GetNodeByURL(url)); + + EXPECT_TRUE(new_node->id() != root->id() && + new_node->id() != model.other_node()->id()); +} + +TEST_F(BookmarkBarModelTest, AddGroup) { + BookmarkBarNode* root = model.GetBookmarkBarNode(); + const std::wstring title(L"foo"); + + BookmarkBarNode* new_node = model.AddGroup(root, 0, title); + AssertObserverCount(1, 0, 0, 0); + observer_details.AssertEquals(root, NULL, 0, -1); + + ASSERT_EQ(1, root->GetChildCount()); + ASSERT_EQ(title, new_node->GetTitle()); + ASSERT_EQ(history::StarredEntry::USER_GROUP, new_node->GetType()); + + EXPECT_TRUE(new_node->id() != root->id() && + new_node->id() != model.other_node()->id()); + + // Add another group, just to make sure group_ids are incremented correctly. + ClearCounts(); + BookmarkBarNode* new_node2 = model.AddGroup(root, 0, title); + AssertObserverCount(1, 0, 0, 0); + observer_details.AssertEquals(root, NULL, 0, -1); +} + +TEST_F(BookmarkBarModelTest, RemoveURL) { + BookmarkBarNode* root = model.GetBookmarkBarNode(); + const std::wstring title(L"foo"); + const GURL url("http://foo.com"); + BookmarkBarNode* new_node = model.AddURL(root, 0, title, url); + ClearCounts(); + + model.Remove(root, 0); + ASSERT_EQ(0, root->GetChildCount()); + AssertObserverCount(0, 0, 1, 0); + observer_details.AssertEquals(root, NULL, 0, -1); + + // Make sure there is no mapping for the URL. + ASSERT_TRUE(model.GetNodeByURL(url) == NULL); +} + +TEST_F(BookmarkBarModelTest, RemoveGroup) { + BookmarkBarNode* root = model.GetBookmarkBarNode(); + BookmarkBarNode* group = model.AddGroup(root, 0, L"foo"); + + ClearCounts(); + + // Add a URL as a child. + const std::wstring title(L"foo"); + const GURL url("http://foo.com"); + BookmarkBarNode* new_node = model.AddURL(group, 0, title, url); + + ClearCounts(); + + // Now remove the group. + model.Remove(root, 0); + ASSERT_EQ(0, root->GetChildCount()); + AssertObserverCount(0, 0, 1, 0); + observer_details.AssertEquals(root, NULL, 0, -1); + + // Make sure there is no mapping for the URL. + ASSERT_TRUE(model.GetNodeByURL(url) == NULL); +} + +TEST_F(BookmarkBarModelTest, SetTitle) { + BookmarkBarNode* root = model.GetBookmarkBarNode(); + std::wstring title(L"foo"); + const GURL url("http://foo.com"); + BookmarkBarNode* node = model.AddURL(root, 0, title, url); + + ClearCounts(); + + title = L"foo2"; + model.SetTitle(node, title); + AssertObserverCount(0, 0, 0, 1); + observer_details.AssertEquals(node, NULL, -1, -1); + EXPECT_EQ(title, node->GetTitle()); +} + +TEST_F(BookmarkBarModelTest, Move) { + BookmarkBarNode* root = model.GetBookmarkBarNode(); + std::wstring title(L"foo"); + const GURL url("http://foo.com"); + BookmarkBarNode* node = model.AddURL(root, 0, title, url); + BookmarkBarNode* group1 = model.AddGroup(root, 0, L"foo"); + ClearCounts(); + + model.Move(node, group1, 0); + + AssertObserverCount(0, 1, 0, 0); + observer_details.AssertEquals(root, group1, 1, 0); + EXPECT_TRUE(group1 == node->GetParent()); + EXPECT_EQ(1, root->GetChildCount()); + EXPECT_EQ(group1, root->GetChild(0)); + EXPECT_EQ(1, group1->GetChildCount()); + EXPECT_EQ(node, group1->GetChild(0)); + + // And remove the group. + ClearCounts(); + model.Remove(root, 0); + AssertObserverCount(0, 0, 1, 0); + observer_details.AssertEquals(root, NULL, 0, -1); + EXPECT_TRUE(model.GetNodeByURL(url) == NULL); + EXPECT_EQ(0, root->GetChildCount()); +} + +// Tests that adding a URL to a folder updates the last modified time. +TEST_F(BookmarkBarModelTest, ParentForNewNodes) { + ASSERT_EQ(model.GetBookmarkBarNode(), model.GetParentForNewNodes()); + + const std::wstring title(L"foo"); + const GURL url("http://foo.com"); + + BookmarkBarNode* new_node = + model.AddURL(model.other_node(), 0, title, url); + + ASSERT_EQ(model.other_node(), model.GetParentForNewNodes()); +} + +// Make sure recently modified stays in sync when adding a URL. +TEST_F(BookmarkBarModelTest, MostRecentlyModifiedGroups) { + // Add a group. + BookmarkBarNode* group = model.AddGroup(model.other_node(), 0, L"foo"); + // Add a URL to it. + model.AddURL(group, 0, L"blah", GURL("http://foo.com")); + + // Make sure group is in the most recently modified. + std::vector<BookmarkBarNode*> most_recent_groups = + model.GetMostRecentlyModifiedGroups(1); + ASSERT_EQ(1, most_recent_groups.size()); + ASSERT_EQ(group, most_recent_groups[0]); + + // Nuke the group and do another fetch, making sure group isn't in the + // returned list. + model.Remove(group->GetParent(), 0); + most_recent_groups = model.GetMostRecentlyModifiedGroups(1); + ASSERT_EQ(1, most_recent_groups.size()); + ASSERT_TRUE(most_recent_groups[0] != group); +} + +// Make sure MostRecentlyAddedEntries stays in sync. +TEST_F(BookmarkBarModelTest, MostRecentlyAddedEntries) { + // Add a couple of nodes such that the following holds for the time of the + // nodes: n1 > n2 > n3 > n4. + Time base_time = Time::Now(); + BookmarkBarNode* n1 = model.AddURL( + model.GetBookmarkBarNode(), 0, L"blah", GURL("http://foo.com/0")); + BookmarkBarNode* n2 = model.AddURL( + model.GetBookmarkBarNode(), 1, L"blah", GURL("http://foo.com/1")); + BookmarkBarNode* n3 = model.AddURL( + model.GetBookmarkBarNode(), 2, L"blah", GURL("http://foo.com/2")); + BookmarkBarNode* n4 = model.AddURL( + model.GetBookmarkBarNode(), 3, L"blah", GURL("http://foo.com/3")); + n1->date_added_ = base_time + TimeDelta::FromDays(4); + n2->date_added_ = base_time + TimeDelta::FromDays(3); + n3->date_added_ = base_time + TimeDelta::FromDays(2); + n4->date_added_ = base_time + TimeDelta::FromDays(1); + + // Make sure order is honored. + std::vector<BookmarkBarNode*> recently_added; + model.GetMostRecentlyAddedEntries(2, &recently_added); + ASSERT_EQ(2, recently_added.size()); + ASSERT_TRUE(n1 == recently_added[0]); + ASSERT_TRUE(n2 == recently_added[1]); + + // swap 1 and 2, then check again. + recently_added.clear(); + std::swap(n1->date_added_, n2->date_added_); + model.GetMostRecentlyAddedEntries(4, &recently_added); + ASSERT_EQ(4, recently_added.size()); + ASSERT_TRUE(n2 == recently_added[0]); + ASSERT_TRUE(n1 == recently_added[1]); + ASSERT_TRUE(n3 == recently_added[2]); + ASSERT_TRUE(n4 == recently_added[3]); +} + +// Makes sure GetBookmarksMatchingText works. +TEST_F(BookmarkBarModelTest, GetBookmarksMatchingText) { + // Add two urls with titles 'blah' and 'x' and one folder with the title + // 'blah'. + BookmarkBarNode* n1 = model.AddURL( + model.GetBookmarkBarNode(), 0, L"blah", GURL("http://foo.com/0")); + BookmarkBarNode* n2 = model.AddURL( + model.GetBookmarkBarNode(), 1, L"x", GURL("http://foo.com/1")); + model.AddGroup(model.GetBookmarkBarNode(), 2, L"blah"); + + // Make sure we don't get back the folder. + std::vector<BookmarkBarModel::TitleMatch> results; + model.GetBookmarksMatchingText(L"blah", 2, &results); + ASSERT_EQ(1U, results.size()); + EXPECT_EQ(n1, results[0].node); + results.clear(); + + model.GetBookmarksMatchingText(L"x", 2, &results); + ASSERT_EQ(1U, results.size()); + EXPECT_EQ(n2, results[0].node); +} + +namespace { + +// See comment in PopulateNodeFromString. +typedef ChromeViews::TreeNodeWithValue<history::StarredEntry::Type> TestNode; + +// Does the work of PopulateNodeFromString. index gives the index of the current +// element in description to process. +static void PopulateNodeImpl(const std::vector<std::wstring>& description, + size_t* index, + TestNode* parent) { + while (*index < description.size()) { + const std::wstring& element = description[*index]; + (*index)++; + if (element == L"[") { + // Create a new group and recurse to add all the children. + // Groups are given a unique named by way of an ever increasing integer + // value. The groups need not have a name, but one is assigned to help + // in debugging. + static int next_group_id = 1; + TestNode* new_node = + new TestNode(IntToWString(next_group_id++), + history::StarredEntry::USER_GROUP); + parent->Add(parent->GetChildCount(), new_node); + PopulateNodeImpl(description, index, new_node); + } else if (element == L"]") { + // End the current group. + return; + } else { + // Add a new URL. + + // All tokens must be space separated. If there is a [ or ] in the name it + // likely means a space was forgotten. + DCHECK(element.find('[') == std::string::npos); + DCHECK(element.find(']') == std::string::npos); + parent->Add(parent->GetChildCount(), + new TestNode(element, history::StarredEntry::URL)); + } + } +} + +// Creates and adds nodes to parent based on description. description consists +// of the following tokens (all space separated): +// [ : creates a new USER_GROUP node. All elements following the [ until the +// next balanced ] is encountered are added as children to the node. +// ] : closes the last group created by [ so that any further nodes are added +// to the current groups parent. +// text: creates a new URL node. +// For example, "a [b] c" creates the following nodes: +// a 1 c +// | +// b +// In words: a node of type URL with the title a, followed by a group node with +// the title 1 having the single child of type url with name b, followed by +// the url node with the title c. +// +// NOTE: each name must be unique, and groups are assigned a unique title by way +// of an increasing integer. +static void PopulateNodeFromString(const std::wstring& description, + TestNode* parent) { + std::vector<std::wstring> elements; + size_t index = 0; + SplitStringAlongWhitespace(description, &elements); + PopulateNodeImpl(elements, &index, parent); +} + +// Populates the BookmarkBarNode with the children of parent. +static void PopulateBookmarkBarNode(TestNode* parent, + BookmarkBarModel* model, + BookmarkBarNode* bb_node) { + for (int i = 0; i < parent->GetChildCount(); ++i) { + TestNode* child = parent->GetChild(i); + if (child->value == history::StarredEntry::USER_GROUP) { + BookmarkBarNode* new_bb_node = + model->AddGroup(bb_node, i, child->GetTitle()); + PopulateBookmarkBarNode(child, model, new_bb_node); + } else { + model->AddURL(bb_node, i, child->GetTitle(), + GURL("http://" + WideToASCII(child->GetTitle()))); + } + } +} + +} // namespace + +// Test class that creates a BookmarkBarModel with a real history backend. +class BookmarkBarModelTestWithProfile : public testing::Test, + public BookmarkBarModelObserver { + public: + virtual void SetUp() { + } + + virtual void TearDown() { + profile_.reset(NULL); + } + + // The profile. + scoped_ptr<TestingProfile> profile_; + + protected: + // Verifies the contents of the bookmark bar node match the contents of the + // TestNode. + void VerifyModelMatchesNode(TestNode* expected, BookmarkBarNode* actual) { + ASSERT_EQ(expected->GetChildCount(), actual->GetChildCount()); + for (int i = 0; i < expected->GetChildCount(); ++i) { + TestNode* expected_child = expected->GetChild(i); + BookmarkBarNode* actual_child = actual->GetChild(i); + ASSERT_EQ(expected_child->GetTitle(), actual_child->GetTitle()); + if (expected_child->value == history::StarredEntry::USER_GROUP) { + ASSERT_TRUE(actual_child->GetType() == + history::StarredEntry::USER_GROUP); + // Recurse throught children. + VerifyModelMatchesNode(expected_child, actual_child); + if (HasFatalFailure()) + return; + } else { + // No need to check the URL, just the title is enough. + ASSERT_TRUE(actual_child->GetType() == + history::StarredEntry::URL); + } + } + } + + void BlockTillBookmarkModelLoaded() { + bb_model_ = profile_->GetBookmarkBarModel(); + if (!bb_model_->IsLoaded()) + BlockTillLoaded(bb_model_); + else + bb_model_->AddObserver(this); + } + + // Destroys the current profile, creates a new one and creates the history + // service. + void RecreateProfile() { + // Need to shutdown the old one before creating a new one. + profile_.reset(NULL); + profile_.reset(new TestingProfile()); + profile_->CreateHistoryService(true); + } + + BookmarkBarModel* bb_model_; + + private: + // Blocks until the BookmarkBarModel has finished loading. + void BlockTillLoaded(BookmarkBarModel* model) { + model->AddObserver(this); + MessageLoop::current()->Run(); + } + + // BookmarkBarModelObserver methods. + virtual void Loaded(BookmarkBarModel* model) { + // Balances the call in BlockTillLoaded. + MessageLoop::current()->Quit(); + } + virtual void BookmarkNodeMoved(BookmarkBarModel* model, + BookmarkBarNode* old_parent, + int old_index, + BookmarkBarNode* new_parent, + int new_index) {} + virtual void BookmarkNodeAdded(BookmarkBarModel* model, + BookmarkBarNode* parent, + int index) {} + virtual void BookmarkNodeRemoved(BookmarkBarModel* model, + BookmarkBarNode* parent, + int index) {} + virtual void BookmarkNodeChanged(BookmarkBarModel* model, + BookmarkBarNode* node) {} + virtual void BookmarkNodeFavIconLoaded(BookmarkBarModel* model, + BookmarkBarNode* node) {} + + MessageLoopForUI message_loop_; +}; + +// Creates a set of nodes in the bookmark bar model, then recreates the +// bookmark bar model which triggers loading from the db and checks the loaded +// structure to make sure it is what we first created. +TEST_F(BookmarkBarModelTestWithProfile, CreateAndRestore) { + struct TestData { + // Structure of the children of the bookmark bar model node. + const std::wstring bbn_contents; + // Structure of the children of the other node. + const std::wstring other_contents; + } data[] = { + // See PopulateNodeFromString for a description of these strings. + { L"", L"" }, + { L"a", L"b" }, + { L"a [ b ]", L"" }, + { L"", L"[ b ] a [ c [ d e [ f ] ] ]" }, + { L"a [ b ]", L"" }, + { L"a b c [ d e [ f ] ]", L"g h i [ j k [ l ] ]"}, + }; + for (int i = 0; i < arraysize(data); ++i) { + // Recreate the profile. We need to reset with NULL first so that the last + // HistoryService releases the locks on the files it creates and we can + // delete them. + profile_.reset(NULL); + profile_.reset(new TestingProfile()); + profile_->CreateBookmarkBarModel(true); + profile_->CreateHistoryService(true); + BlockTillBookmarkModelLoaded(); + + TestNode bbn; + PopulateNodeFromString(data[i].bbn_contents, &bbn); + PopulateBookmarkBarNode(&bbn, bb_model_, bb_model_->GetBookmarkBarNode()); + + TestNode other; + PopulateNodeFromString(data[i].other_contents, &other); + PopulateBookmarkBarNode(&other, bb_model_, bb_model_->other_node()); + + profile_->CreateBookmarkBarModel(false); + BlockTillBookmarkModelLoaded(); + + VerifyModelMatchesNode(&bbn, bb_model_->GetBookmarkBarNode()); + VerifyModelMatchesNode(&other, bb_model_->other_node()); + } +} + +// Test class that creates a BookmarkBarModel with a real history backend. +class BookmarkBarModelTestWithProfile2 : + public BookmarkBarModelTestWithProfile { + public: + virtual void SetUp() { + profile_.reset(new TestingProfile()); + } + + protected: + // Verifies the state of the model matches that of the state in the saved + // history file. + void VerifyExpectedState() { + // Here's the structure we expect: + // bbn + // www.google.com - Google + // F1 + // http://www.google.com/intl/en/ads/ - Google Advertising + // F11 + // http://www.google.com/services/ - Google Business Solutions + // other + // OF1 + // http://www.google.com/intl/en/about.html - About Google + BookmarkBarNode* bbn = bb_model_->GetBookmarkBarNode(); + ASSERT_EQ(2, bbn->GetChildCount()); + + BookmarkBarNode* child = bbn->GetChild(0); + ASSERT_EQ(history::StarredEntry::URL, child->GetType()); + ASSERT_EQ(L"Google", child->GetTitle()); + ASSERT_TRUE(child->GetURL() == GURL("http://www.google.com")); + + child = bbn->GetChild(1); + ASSERT_TRUE(child->is_folder()); + ASSERT_EQ(L"F1", child->GetTitle()); + ASSERT_EQ(2, child->GetChildCount()); + + BookmarkBarNode* parent = child; + child = parent->GetChild(0); + ASSERT_EQ(history::StarredEntry::URL, child->GetType()); + ASSERT_EQ(L"Google Advertising", child->GetTitle()); + ASSERT_TRUE(child->GetURL() == GURL("http://www.google.com/intl/en/ads/")); + + child = parent->GetChild(1); + ASSERT_TRUE(child->is_folder()); + ASSERT_EQ(L"F11", child->GetTitle()); + ASSERT_EQ(1, child->GetChildCount()); + + parent = child; + child = parent->GetChild(0); + ASSERT_EQ(history::StarredEntry::URL, child->GetType()); + ASSERT_EQ(L"Google Business Solutions", child->GetTitle()); + ASSERT_TRUE(child->GetURL() == GURL("http://www.google.com/services/")); + + parent = bb_model_->other_node(); + ASSERT_EQ(2, parent->GetChildCount()); + + child = parent->GetChild(0); + ASSERT_TRUE(child->is_folder()); + ASSERT_EQ(L"OF1", child->GetTitle()); + ASSERT_EQ(0, child->GetChildCount()); + + child = parent->GetChild(1); + ASSERT_EQ(history::StarredEntry::URL, child->GetType()); + ASSERT_EQ(L"About Google", child->GetTitle()); + ASSERT_TRUE(child->GetURL() == + GURL("http://www.google.com/intl/en/about.html")); + + ASSERT_TRUE(bb_model_->GetNodeByURL(GURL("http://www.google.com")) != NULL); + } +}; + +// Tests migrating bookmarks from db into file. This copies an old history db +// file containing bookmarks and make sure they are loaded correctly and +// persisted correctly. +TEST_F(BookmarkBarModelTestWithProfile2, MigrateFromDBToFileTest) { + // Copy db file over that contains starred table. + std::wstring old_history_path; + PathService::Get(chrome::DIR_TEST_DATA, &old_history_path); + file_util::AppendToPath(&old_history_path, L"bookmarks"); + file_util::AppendToPath(&old_history_path, L"History_with_starred"); + std::wstring new_history_path = profile_->GetPath(); + file_util::Delete(new_history_path, true); + file_util::CreateDirectory(new_history_path); + file_util::AppendToPath(&new_history_path, chrome::kHistoryFilename); + file_util::CopyFile(old_history_path, new_history_path); + + // Create the history service making sure it doesn't blow away the file we + // just copied. + profile_->CreateHistoryService(false); + profile_->CreateBookmarkBarModel(true); + BlockTillBookmarkModelLoaded(); + + // Make sure we loaded OK. + VerifyExpectedState(); + if (HasFatalFailure()) + return; + + // Create again. This time we shouldn't load from history at all. + profile_->CreateBookmarkBarModel(false); + BlockTillBookmarkModelLoaded(); + + // Make sure we loaded OK. + VerifyExpectedState(); + if (HasFatalFailure()) + return; + + // Recreate the history service (with a clean db). Do this just to make sure + // we're loading correctly from the bookmarks file. + profile_->CreateHistoryService(true); + profile_->CreateBookmarkBarModel(false); + BlockTillBookmarkModelLoaded(); + VerifyExpectedState(); +} + +// Simple test that removes a bookmark. This test exercises the code paths in +// History that block till bookmark bar model is loaded. +TEST_F(BookmarkBarModelTestWithProfile2, RemoveNotification) { + profile_->CreateHistoryService(false); + profile_->CreateBookmarkBarModel(true); + BlockTillBookmarkModelLoaded(); + + // Add a URL. + GURL url("http://www.google.com"); + bb_model_->SetURLStarred(url, std::wstring(), true); + + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS)->AddPage( + url, NULL, 1, GURL(), PageTransition::TYPED, + HistoryService::RedirectList()); + + // This won't actually delete the URL, rather it'll empty out the visits. + // This triggers blocking on the BookmarkBarModel. + profile_->GetHistoryService(Profile::EXPLICIT_ACCESS)->DeleteURL(url); +} diff --git a/chrome/browser/bookmarks/bookmark_codec.cc b/chrome/browser/bookmarks/bookmark_codec.cc new file mode 100644 index 0000000..93874f3 --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_codec.cc @@ -0,0 +1,195 @@ +// 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. + +#include "chrome/browser/bookmarks/bookmark_codec.h" + +#include "base/string_util.h" +#include "base/values.h" +#include "chrome/browser/bookmarks/bookmark_bar_model.h" +#include "googleurl/src/gurl.h" + +#include "generated_resources.h" + +// Key names. +static const wchar_t* kRootsKey = L"roots"; +static const wchar_t* kRootFolderNameKey = L"bookmark_bar"; +static const wchar_t* kOtherBookmarFolderNameKey = L"other"; +static const wchar_t* kVersionKey = L"version"; +static const wchar_t* kTypeKey = L"type"; +static const wchar_t* kNameKey = L"name"; +static const wchar_t* kDateAddedKey = L"date_added"; +static const wchar_t* kURLKey = L"url"; +static const wchar_t* kDateModifiedKey = L"date_modified"; +static const wchar_t* kChildrenKey = L"children"; + +// Possible values for kTypeKey. +static const wchar_t* kTypeURL = L"url"; +static const wchar_t* kTypeFolder = L"folder"; + +// Current version of the file. +static const int kCurrentVersion = 1; + +Value* BookmarkCodec::Encode(BookmarkBarModel* model) { + return Encode(model->GetBookmarkBarNode(), model->other_node()); +} + +Value* BookmarkCodec::Encode(BookmarkBarNode* bookmark_bar_node, + BookmarkBarNode* other_folder_node) { + DictionaryValue* roots = new DictionaryValue(); + roots->Set(kRootFolderNameKey, EncodeNode(bookmark_bar_node)); + roots->Set(kOtherBookmarFolderNameKey, EncodeNode(other_folder_node)); + + DictionaryValue* main = new DictionaryValue(); + main->SetInteger(kVersionKey, kCurrentVersion); + main->Set(kRootsKey, roots); + return main; +} + +bool BookmarkCodec::Decode(BookmarkBarModel* model, const Value& value) { + if (value.GetType() != Value::TYPE_DICTIONARY) + return false; // Unexpected type. + + const DictionaryValue& d_value = static_cast<const DictionaryValue&>(value); + + int version; + if (!d_value.GetInteger(kVersionKey, &version) || version != kCurrentVersion) + return false; // Unknown version. + + Value* roots; + if (!d_value.Get(kRootsKey, &roots)) + return false; // No roots. + + if (roots->GetType() != Value::TYPE_DICTIONARY) + return false; // Invalid type for roots. + + DictionaryValue* roots_d_value = static_cast<DictionaryValue*>(roots); + Value* root_folder_value; + Value* other_folder_value; + if (!roots_d_value->Get(kRootFolderNameKey, &root_folder_value) || + root_folder_value->GetType() != Value::TYPE_DICTIONARY || + !roots_d_value->Get(kOtherBookmarFolderNameKey, &other_folder_value) || + other_folder_value->GetType() != Value::TYPE_DICTIONARY) + return false; // Invalid type for root folder and/or other folder. + + DecodeNode(model, *static_cast<DictionaryValue*>(root_folder_value), + NULL, model->GetBookmarkBarNode()); + DecodeNode(model, *static_cast<DictionaryValue*>(other_folder_value), + NULL, model->other_node()); + // Need to reset the type as decoding resets the type to FOLDER. Similarly + // we need to reset the title as the title is persisted and restored from + // the file. + model->GetBookmarkBarNode()->type_ = history::StarredEntry::BOOKMARK_BAR; + model->other_node()->type_ = history::StarredEntry::OTHER; + model->GetBookmarkBarNode()->SetTitle( + l10n_util::GetString(IDS_BOOMARK_BAR_FOLDER_NAME)); + model->other_node()->SetTitle( + l10n_util::GetString(IDS_BOOMARK_BAR_OTHER_FOLDER_NAME)); + return true; +} + +Value* BookmarkCodec::EncodeNode(BookmarkBarNode* node) { + DictionaryValue* value = new DictionaryValue(); + value->SetString(kNameKey, node->GetTitle()); + value->SetString(kDateAddedKey, + Int64ToWString(node->date_added().ToInternalValue())); + if (node->GetType() == history::StarredEntry::URL) { + value->SetString(kTypeKey, kTypeURL); + value->SetString(kURLKey, + UTF8ToWide(node->GetURL().possibly_invalid_spec())); + } else { + value->SetString(kTypeKey, kTypeFolder); + value->SetString(kDateModifiedKey, + Int64ToWString(node->date_group_modified(). + ToInternalValue())); + + ListValue* child_values = new ListValue(); + value->Set(kChildrenKey, child_values); + for (int i = 0; i < node->GetChildCount(); ++i) + child_values->Append(EncodeNode(node->GetChild(i))); + } + return value; +} + +bool BookmarkCodec::DecodeChildren(BookmarkBarModel* model, + const ListValue& child_value_list, + BookmarkBarNode* parent) { + for (size_t i = 0; i < child_value_list.GetSize(); ++i) { + Value* child_value; + if (!child_value_list.Get(i, &child_value)) + return false; + + if (child_value->GetType() != Value::TYPE_DICTIONARY) + return false; + + if (!DecodeNode(model, *static_cast<DictionaryValue*>(child_value), parent, + NULL)) { + return false; + } + } + return true; +} + +bool BookmarkCodec::DecodeNode(BookmarkBarModel* model, + const DictionaryValue& value, + BookmarkBarNode* parent, + BookmarkBarNode* node) { + bool created_node = (node == NULL); + std::wstring title; + if (!value.GetString(kNameKey, &title)) + return false; + + // TODO(sky): this should be more flexible. Don't hoark if we can't parse it + // all. + std::wstring date_added_string; + if (!value.GetString(kDateAddedKey, &date_added_string)) + return false; + + std::wstring type_string; + if (!value.GetString(kTypeKey, &type_string)) + return false; + + if (type_string != kTypeURL && type_string != kTypeFolder) + return false; // Unknown type. + + if (type_string == kTypeURL) { + std::wstring url_string; + if (!value.GetString(kURLKey, &url_string)) + return false; + // TODO(sky): this should ignore the node if not a valid URL. + if (!node) + node = new BookmarkBarNode(model, GURL(url_string)); + if (parent) + parent->Add(parent->GetChildCount(), node); + node->type_ = history::StarredEntry::URL; + } else { + std::wstring last_modified_date; + if (!value.GetString(kDateModifiedKey, &last_modified_date)) + return false; + + Value* child_values; + if (!value.Get(kChildrenKey, &child_values)) + return false; + + if (child_values->GetType() != Value::TYPE_LIST) + return false; + + if (!node) + node = new BookmarkBarNode(model, GURL()); + node->type_ = history::StarredEntry::USER_GROUP; + node->date_group_modified_ = + Time::FromInternalValue(StringToInt64(last_modified_date)); + + if (parent) + parent->Add(parent->GetChildCount(), node); + + if (!DecodeChildren(model, *static_cast<ListValue*>(child_values), node)) + return false; + } + + node->SetTitle(title); + node->date_added_ = + Time::FromInternalValue(StringToInt64(date_added_string)); + return true; +} + diff --git a/chrome/browser/bookmarks/bookmark_codec.h b/chrome/browser/bookmarks/bookmark_codec.h new file mode 100644 index 0000000..1470f2c --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_codec.h @@ -0,0 +1,67 @@ +// 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. + +// BookmarkCodec is responsible for encoding and decoding the BookmarkBarModel +// into JSON values. The encoded values are written to disk via the +// BookmarkService. + +#ifndef CHROME_BROWSER_BOOKMARKS_BOOKMARK_CODEC_H_ +#define CHROME_BROWSER_BOOKMARKS_BOOKMARK_CODEC_H_ + +#include "base/basictypes.h" + +class BookmarkBarModel; +class BookmarkBarNode; +class DictionaryValue; +class ListValue; +class Value; + +// BookmarkCodec is responsible for encoding/decoding bookmarks into JSON +// values. BookmarkCodec is used by BookmarkService. + +class BookmarkCodec { + public: + BookmarkCodec() {} + + // Encodes the model to a JSON value. It's up to the caller to delete the + // returned object. This is invoked to encode the contents of the bookmark bar + // model and is currently a convenience to invoking Encode that takes the + // bookmark bar node and other folder node. + Value* Encode(BookmarkBarModel* model); + + // Encodes the bookmark bar and other folders returning the JSON value. It's + // up to the caller to delete the returned object. + // This method is public for use by StarredURLDatabase in migrating the + // bookmarks out of the database. + Value* Encode(BookmarkBarNode* bookmark_bar_node, + BookmarkBarNode* other_folder_node); + + // Decodes the previously encoded value to the specified model. Returns true + // on success, false otherwise. If there is an error (such as unexpected + // version) all children are removed from the bookmark bar and other folder + // nodes. + bool Decode(BookmarkBarModel* model, const Value& value); + + private: + // Encodes node and all its children into a Value object and returns it. + // The caller takes ownership of the returned object. + Value* EncodeNode(BookmarkBarNode* node); + + // Decodes the children of the specified node. Returns true on success. + bool DecodeChildren(BookmarkBarModel* model, + const ListValue& child_value_list, + BookmarkBarNode* parent); + + // Decodes the supplied node from the supplied value. Child nodes are + // created appropriately by way of DecodeChildren. If node is NULL a new + // node is created and added to parent, otherwise node is used. + bool DecodeNode(BookmarkBarModel* model, + const DictionaryValue& value, + BookmarkBarNode* parent, + BookmarkBarNode* node); + + DISALLOW_COPY_AND_ASSIGN(BookmarkCodec); +}; + +#endif // CHROME_BROWSER_BOOKMARKS_BOOKMARK_CODEC_H_ diff --git a/chrome/browser/bookmarks/bookmark_drag_data.cc b/chrome/browser/bookmarks/bookmark_drag_data.cc new file mode 100644 index 0000000..61c9ccc --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_drag_data.cc @@ -0,0 +1,118 @@ +// 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. + +#include "chrome/browser/bookmarks/bookmark_drag_data.h" + +#include "base/pickle.h" +#include "chrome/browser/bookmarks/bookmark_bar_model.h" +#include "chrome/common/os_exchange_data.h" + +static CLIPFORMAT clipboard_format = 0; + +static void RegisterFormat() { + if (clipboard_format == 0) { + clipboard_format = RegisterClipboardFormat(L"chrome/x-bookmark-entry"); + DCHECK(clipboard_format); + } +} + +BookmarkDragData::BookmarkDragData() : is_url(false), is_valid(false) { +} + +BookmarkDragData::BookmarkDragData(BookmarkBarNode* node) + : is_url(node->GetType() == history::StarredEntry::URL), + url(node->GetURL()), + title(node->GetTitle()), + is_valid(true), + id_(node->id()) { + if (!is_url) + AddChildren(node); +} + +void BookmarkDragData::Write(OSExchangeData* data) const { + RegisterFormat(); + + DCHECK(data); + + if (is_url) { + data->SetURL(url, title); + } + Pickle data_pickle; + WriteToPickle(&data_pickle); + data->SetPickledData(clipboard_format, data_pickle); +} + +bool BookmarkDragData::Read(const OSExchangeData& data) { + RegisterFormat(); + + is_valid = data.GetURLAndTitle(&url, &title) && url.is_valid(); + is_url = is_valid; + profile_id.clear(); + + if (data.HasFormat(clipboard_format)) { + Pickle drag_data_pickle; + if (data.GetPickledData(clipboard_format, &drag_data_pickle)) { + void* data_iterator = NULL; + if (ReadFromPickle(&drag_data_pickle, &data_iterator)) { + is_valid = true; + } + } + } + return is_valid; +} + +BookmarkBarNode* BookmarkDragData::GetNode(BookmarkBarModel* model) const { + DCHECK(!is_url && id_ && is_valid); + return model->GetNodeByID(id_); +} + +void BookmarkDragData::WriteToPickle(Pickle* pickle) const { + pickle->WriteBool(is_url); + pickle->WriteWString(profile_id); + pickle->WriteString(url.spec()); + pickle->WriteWString(title); + if (!is_url) { + pickle->WriteInt(id_); + pickle->WriteInt(static_cast<int>(children.size())); + for (std::vector<BookmarkDragData>::const_iterator i = children.begin(); + i != children.end(); ++i) { + i->WriteToPickle(pickle); + } + } +} + +bool BookmarkDragData::ReadFromPickle(Pickle* pickle, void** iterator) { + std::string url_spec; + is_valid = false; + if (!pickle->ReadBool(iterator, &is_url) || + !pickle->ReadWString(iterator, &profile_id) || + !pickle->ReadString(iterator, &url_spec) || + !pickle->ReadWString(iterator, &title)) { + return false; + } + url = GURL(url_spec); + if (!is_url) { + id_ = 0; + children.clear(); + if (!pickle->ReadInt(iterator, &id_)) + return false; + int children_count; + if (!pickle->ReadInt(iterator, &children_count)) + return false; + children.resize(children_count); + for (std::vector<BookmarkDragData>::iterator i = children.begin(); + i != children.end(); ++i) { + if (!i->ReadFromPickle(pickle, iterator)) + return false; + } + } + is_valid = true; + return true; +} + +void BookmarkDragData::AddChildren(BookmarkBarNode* node) { + for (int i = 0, max = node->GetChildCount(); i < max; ++i) + children.push_back(BookmarkDragData(node->GetChild(i))); +} + diff --git a/chrome/browser/bookmarks/bookmark_drag_data.h b/chrome/browser/bookmarks/bookmark_drag_data.h new file mode 100644 index 0000000..fd967eb --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_drag_data.h @@ -0,0 +1,81 @@ +// 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. + +#ifndef CHROME_BROWSER_BOOKMARKS_BOOKMARK_DRAG_DATA_ +#define CHROME_BROWSER_BOOKMARKS_BOOKMARK_DRAG_DATA_ + +#include <vector> + +#include "chrome/browser/history/history.h" +#include "googleurl/src/gurl.h" + +class BookmarkBarModel; +class BookmarkBarNode; +class OSExchangeData; +class Pickle; + +// BookmarkDragData is used by the bookmark bar to represent a dragged +// URL or starred group on the clipboard during drag and drop. +// +// Typical usage when writing data for a drag is: +// BookmarkDragData data(node_user_is_dragging); +// data.profile_id = profile_id; +// data.Write(os_exchange_data_for_drag); +// +// Typical usage to read is: +// BookmarkDragData data; +// if (data.Read(os_exchange_data)) +// // data is valid + +struct BookmarkDragData { + BookmarkDragData(); + + // Created a BookmarkDragData populated from node. + explicit BookmarkDragData(BookmarkBarNode* node); + + // Writes this BookmarkDragData to data. If BookmarkDragData is a URL, + // this writes out the URL and URL title clipboard data as well. + void Write(OSExchangeData* data) const; + + // Restores this data from the clipboard, returning true on success. + bool Read(const OSExchangeData& data); + + // Returns the node represented by this drag data from root. If the + // path can not be found, NULL is returned. + // + // This is only valid for groups. + BookmarkBarNode* BookmarkDragData::GetNode(BookmarkBarModel* model) const; + + // If true, this entry represents a StarredEntry of type URL. + bool is_url; + + // ID of the profile we originated from. + std::wstring profile_id; + + // The URL, only valid if is_url is true. + GURL url; + + // Title of the entry + std::wstring title; + + // Children, only used for non-URL nodes. + std::vector<BookmarkDragData> children; + + // If true our data is valid. + bool is_valid; + + private: + // Writes the data to a Pickle. + void WriteToPickle(Pickle* pickle) const; + + bool ReadFromPickle(Pickle* pickle, void** iterator); + + // Adds to children an entry for each child of node. + void AddChildren(BookmarkBarNode* node); + + // ID (node->id()) of the node this BookmarkDragData was created from. + int id_; +}; + +#endif // CHROME_BROWSER_BOOKMARKS_BOOKMARK_DRAG_DATA_ diff --git a/chrome/browser/bookmarks/bookmark_drag_data_unittest.cc b/chrome/browser/bookmarks/bookmark_drag_data_unittest.cc new file mode 100644 index 0000000..4ebe7b0 --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_drag_data_unittest.cc @@ -0,0 +1,120 @@ +// 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. + +#include "base/scoped_ptr.h" +#include "chrome/browser/bookmarks/bookmark_bar_model.h" +#include "chrome/browser/bookmarks/bookmark_drag_data.h" +#include "chrome/common/os_exchange_data.h" +#include "googleurl/src/gurl.h" +#include "testing/gtest/include/gtest/gtest.h" + +typedef testing::Test BookmarkDragDataTest; + +TEST_F(BookmarkDragDataTest, InitialState) { + BookmarkDragData data; + EXPECT_FALSE(data.is_valid); +} + +TEST_F(BookmarkDragDataTest, BogusRead) { + scoped_refptr<OSExchangeData> data(new OSExchangeData()); + BookmarkDragData drag_data; + drag_data.is_valid = true; + EXPECT_FALSE(drag_data.Read(data.get())); + EXPECT_FALSE(drag_data.is_valid); +} + +TEST_F(BookmarkDragDataTest, URL) { + BookmarkBarModel model(NULL); + BookmarkBarNode* root = model.GetBookmarkBarNode(); + GURL url(GURL("http://foo.com")); + const std::wstring profile_id(L"blah"); + const std::wstring title(L"blah"); + BookmarkBarNode* node = model.AddURL(root, 0, title, url); + BookmarkDragData drag_data(node); + drag_data.profile_id = profile_id; + EXPECT_TRUE(drag_data.url == url); + EXPECT_EQ(title, drag_data.title); + EXPECT_TRUE(drag_data.is_url); + + scoped_refptr<OSExchangeData> data(new OSExchangeData()); + drag_data.Write(data.get()); + + // Now read the data back in. + scoped_refptr<OSExchangeData> data2(new OSExchangeData(data.get())); + BookmarkDragData read_data; + EXPECT_TRUE(read_data.Read(*data2)); + EXPECT_TRUE(read_data.url == url); + EXPECT_EQ(title, read_data.title); + EXPECT_TRUE(read_data.is_valid); + EXPECT_TRUE(read_data.is_url); + + // Writing should also put the URL and title on the clipboard. + GURL read_url; + std::wstring read_title; + EXPECT_TRUE(data2->GetURLAndTitle(&read_url, &read_title)); + EXPECT_TRUE(read_url == url); + EXPECT_EQ(title, read_title); +} + +TEST_F(BookmarkDragDataTest, Group) { + BookmarkBarModel model(NULL); + BookmarkBarNode* root = model.GetBookmarkBarNode(); + BookmarkBarNode* g1 = model.AddGroup(root, 0, L"g1"); + BookmarkBarNode* g11 = model.AddGroup(g1, 0, L"g11"); + BookmarkBarNode* g12 = model.AddGroup(g1, 0, L"g12"); + + BookmarkDragData drag_data(g12); + const std::wstring profile_id(L"blah"); + drag_data.profile_id = profile_id; + EXPECT_EQ(g12->GetTitle(), drag_data.title); + EXPECT_FALSE(drag_data.is_url); + + scoped_refptr<OSExchangeData> data(new OSExchangeData()); + drag_data.Write(data.get()); + + // Now read the data back in. + scoped_refptr<OSExchangeData> data2(new OSExchangeData(data.get())); + BookmarkDragData read_data; + EXPECT_TRUE(read_data.Read(*data2)); + EXPECT_EQ(g12->GetTitle(), read_data.title); + EXPECT_EQ(profile_id, read_data.profile_id); + EXPECT_TRUE(read_data.is_valid); + EXPECT_FALSE(read_data.is_url); + + BookmarkBarNode* r_g12 = read_data.GetNode(&model); + EXPECT_TRUE(g12 == r_g12); +} + +TEST_F(BookmarkDragDataTest, GroupWithChild) { + BookmarkBarModel model(NULL); + BookmarkBarNode* root = model.GetBookmarkBarNode(); + BookmarkBarNode* group = model.AddGroup(root, 0, L"g1"); + + GURL url(GURL("http://foo.com")); + const std::wstring profile_id(L"blah"); + const std::wstring title(L"blah2"); + + model.AddURL(group, 0, title, url); + + BookmarkDragData drag_data(group); + drag_data.profile_id = profile_id; + + scoped_refptr<OSExchangeData> data(new OSExchangeData()); + drag_data.Write(data.get()); + + // Now read the data back in. + scoped_refptr<OSExchangeData> data2(new OSExchangeData(data.get())); + BookmarkDragData read_data; + EXPECT_TRUE(read_data.Read(*data2)); + + EXPECT_EQ(1, read_data.children.size()); + EXPECT_TRUE(read_data.children[0].is_valid); + EXPECT_TRUE(read_data.children[0].is_url); + EXPECT_EQ(title, read_data.children[0].title); + EXPECT_TRUE(url == read_data.children[0].url); + EXPECT_TRUE(read_data.children[0].is_url); + + BookmarkBarNode* r_group = read_data.GetNode(&model); + EXPECT_TRUE(group == r_group); +} diff --git a/chrome/browser/bookmarks/bookmark_storage.cc b/chrome/browser/bookmarks/bookmark_storage.cc new file mode 100644 index 0000000..712b573 --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_storage.cc @@ -0,0 +1,175 @@ +// 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. + +#include "chrome/browser/bookmarks/bookmark_storage.h" + +#include "base/file_util.h" +#include "base/json_writer.h" +#include "base/message_loop.h" +#include "chrome/browser/bookmarks/bookmark_bar_model.h" +#include "chrome/browser/bookmarks/bookmark_codec.h" +#include "chrome/browser/profile.h" +#include "chrome/common/chrome_constants.h" +#include "chrome/common/json_value_serializer.h" + +namespace { + +// Extension used for backup files (copy of main file created during startup). +const wchar_t* const kBackupExtension = L"bak"; + +// Extension for the temporary file. We write to the temp file than move to +// kBookmarksFileName. +const wchar_t* const kTmpExtension = L"tmp"; + +// How often we save. +const int kSaveDelayMS = 2500; + +} // namespace + +// BookmarkStorage ------------------------------------------------------------- + +BookmarkStorage::BookmarkStorage(Profile* profile, BookmarkBarModel* model) + : model_(model), +#pragma warning(suppress: 4355) // Okay to pass "this" here. + save_factory_(this), + backend_thread_(g_browser_process->file_thread()) { + std::wstring path = profile->GetPath(); + file_util::AppendToPath(&path, chrome::kBookmarksFileName); + std::wstring tmp_history_path = profile->GetPath(); + file_util::AppendToPath(&tmp_history_path, chrome::kHistoryBookmarksFileName); + backend_ = new BookmarkStorageBackend(path, tmp_history_path); +} + +void BookmarkStorage::LoadBookmarks(bool load_from_history) { + if (!backend_thread()) { + backend_->Read(scoped_refptr<BookmarkStorage>(this), NULL, + load_from_history); + } else { + backend_thread()->message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(backend_.get(), &BookmarkStorageBackend::Read, + scoped_refptr<BookmarkStorage>(this), + MessageLoop::current(), load_from_history)); + } +} + +void BookmarkStorage::ScheduleSave() { + if (!backend_thread()) { + SaveNow(); + } else if (save_factory_.empty()) { + MessageLoop::current()->PostDelayedTask( + FROM_HERE, save_factory_.NewRunnableMethod(&BookmarkStorage::SaveNow), + kSaveDelayMS); + } +} + +void BookmarkStorage::BookmarkModelDeleted() { + if (!save_factory_.empty()) { + // There's a pending save. We need to save now as otherwise by the time + // SaveNow is invoked the model is gone. + save_factory_.RevokeAll(); + SaveNow(); + } + model_ = NULL; +} + +void BookmarkStorage::LoadedBookmarks(Value* root_value, + bool bookmark_file_exists, + bool loaded_from_history) { + scoped_ptr<Value> value_ref(root_value); + + if (model_) { + if (root_value) { + BookmarkCodec codec; + codec.Decode(model_, *root_value); + } + model_->OnBookmarkStorageLoadedBookmarks(bookmark_file_exists, + loaded_from_history); + } +} + +void BookmarkStorage::SaveNow() { + if (!model_ || !model_->IsLoaded()) { + // We should only get here if we have a valid model and it's finished + // loading. + NOTREACHED(); + return; + } + + BookmarkCodec codec; + Value* value = codec.Encode(model_); + // The backend deletes value in write. + if (!backend_thread()) { + backend_->Write(value); + } else { + backend_thread()->message_loop()->PostTask( + FROM_HERE, + NewRunnableMethod(backend_.get(), &BookmarkStorageBackend::Write, + value)); + } +} + +// BookmarkStorageBackend ------------------------------------------------------ + +BookmarkStorageBackend::BookmarkStorageBackend( + const std::wstring& path, + const std::wstring& tmp_history_path) + : path_(path), + tmp_history_path_(tmp_history_path) { + // Make a backup of the current file. + std::wstring backup_path = path; + file_util::ReplaceExtension(&backup_path, kBackupExtension); + file_util::CopyFile(path, backup_path); +} + +void BookmarkStorageBackend::Write(Value* value) { + DCHECK(value); + + // We own Value. + scoped_ptr<Value> value_ref(value); + + std::string content; + JSONWriter::Write(value, true, &content); + + // Write to a temp file, then rename. + std::wstring tmp_file = path_; + file_util::ReplaceExtension(&tmp_file, kTmpExtension); + + int bytes_written = file_util::WriteFile(tmp_file, content.c_str(), + static_cast<int>(content.length())); + if (bytes_written != -1) { + if (!MoveFileEx(tmp_file.c_str(), path_.c_str(), + MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING)) { + // Rename failed. Try again on the off chance someone has locked either + // file and hope we're successful the second time through. + BOOL move_result = + MoveFileEx(tmp_file.c_str(), path_.c_str(), + MOVEFILE_COPY_ALLOWED | MOVEFILE_REPLACE_EXISTING); + DCHECK(move_result); + } + // Nuke the history file so that we don't attempt to load from it again. + file_util::Delete(tmp_history_path_, false); + } +} + +void BookmarkStorageBackend::Read(scoped_refptr<BookmarkStorage> service, + MessageLoop* message_loop, + bool load_from_history) { + const std::wstring& path = load_from_history ? tmp_history_path_ : path_; + bool bookmark_file_exists = file_util::PathExists(path); + Value* root = NULL; + if (bookmark_file_exists) { + JSONFileValueSerializer serializer(path); + serializer.Deserialize(&root); + } + + // BookmarkStorage takes ownership of root. + if (message_loop) { + message_loop->PostTask(FROM_HERE, NewRunnableMethod( + service.get(), &BookmarkStorage::LoadedBookmarks, root, + bookmark_file_exists, load_from_history)); + } else { + service->LoadedBookmarks(root, bookmark_file_exists, load_from_history); + } +} diff --git a/chrome/browser/bookmarks/bookmark_storage.h b/chrome/browser/bookmarks/bookmark_storage.h new file mode 100644 index 0000000..cf2c3a9 --- /dev/null +++ b/chrome/browser/bookmarks/bookmark_storage.h @@ -0,0 +1,97 @@ +// 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.
+
+#ifndef CHROME_BROWSER_BOOKMARKS_BOOKMARK_STORAGE_H_
+#define CHROME_BROWSER_BOOKMARKS_BOOKMARK_STORAGE_H_
+
+#include "base/ref_counted.h"
+#include "base/task.h"
+#include "chrome/browser/browser_process.h"
+
+class BookmarkBarModel;
+class Profile;
+class Value;
+
+// BookmarkStorage handles reading/write the bookmark bar model. The
+// BookmarkBarModel uses the BookmarkStorage to load bookmarks from
+// disk, as well as notifying the BookmarkStorage every time the model
+// changes.
+//
+// Internally BookmarkStorage uses BookmarkCodec to do the actual read/write.
+
+class BookmarkStorage : public base::RefCountedThreadSafe<BookmarkStorage> {
+ friend class BookmarkStorageBackend;
+
+ public:
+ // Creates a BookmarkStorage for the specified model
+ BookmarkStorage(Profile* profile, BookmarkBarModel* model);
+
+ // Loads the bookmarks into the model, notifying the model when done. If
+ // load_from_history is true, the bookmarks are loaded from the file written
+ // by history (StarredURLDatabase).
+ void LoadBookmarks(bool load_from_history);
+
+ // Schedules saving the bookmark bar model to disk.
+ void ScheduleSave();
+
+ // Notification the bookmark bar model is going to be deleted. If there is
+ // a pending save, it is saved immediately.
+ void BookmarkModelDeleted();
+
+ private:
+ // Callback from backend with the results of the bookmark file.
+ void LoadedBookmarks(Value* root_value,
+ bool bookmark_file_exists,
+ bool loaded_from_history);
+
+ // Schedules a save on the backend thread.
+ void SaveNow();
+
+ // Returns the thread the backend is run on.
+ base::Thread* backend_thread() const { return backend_thread_; }
+
+ // The model. The model is NULL once BookmarkModelDeleted has been invoked.
+ BookmarkBarModel* model_;
+
+ // Used to delay saves.
+ ScopedRunnableMethodFactory<BookmarkStorage> save_factory_;
+
+ // The backend handles actual reading/writing to disk.
+ scoped_refptr<BookmarkStorageBackend> backend_;
+
+ // Thread read/writing is run on. This comes from the profile, and is null
+ // during testing.
+ base::Thread* backend_thread_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkStorage);
+};
+
+// Used to save the bookmarks on the file thread.
+class BookmarkStorageBackend :
+ public base::RefCountedThreadSafe<BookmarkStorageBackend> {
+ public:
+ explicit BookmarkStorageBackend(const std::wstring& path,
+ const std::wstring& tmp_histor_path);
+
+ // Writes the specified value to disk. This takes ownership of |value| and
+ // deletes it when done.
+ void Write(Value* value);
+
+ // Reads the bookmarks from kBookmarksFileName. Notifies |service| with
+ // the results on the specified MessageLoop.
+ void Read(scoped_refptr<BookmarkStorage> service,
+ MessageLoop* message_loop,
+ bool load_from_history);
+
+ private:
+ // Path we read/write to.
+ const std::wstring path_;
+
+ // Path bookmarks are read from if asked to load from history file.
+ const std::wstring tmp_history_path_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkStorageBackend);
+};
+
+#endif // CHROME_BROWSER_BOOKMARKS_BOOKMARK_STORAGE_H_
|