summaryrefslogtreecommitdiffstats
path: root/chrome/browser/history
diff options
context:
space:
mode:
authorinitial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98>2008-07-26 23:55:29 +0000
committerinitial.commit <initial.commit@0039d316-1c4b-4281-b951-d872f2087c98>2008-07-26 23:55:29 +0000
commit09911bf300f1a419907a9412154760efd0b7abc3 (patch)
treef131325fb4e2ad12c6d3504ab75b16dd92facfed /chrome/browser/history
parent586acc5fe142f498261f52c66862fa417c3d52d2 (diff)
downloadchromium_src-09911bf300f1a419907a9412154760efd0b7abc3.zip
chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.tar.gz
chromium_src-09911bf300f1a419907a9412154760efd0b7abc3.tar.bz2
Add chrome to the repository.
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@15 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/history')
-rw-r--r--chrome/browser/history/archived_database.cc126
-rw-r--r--chrome/browser/history/archived_database.h88
-rw-r--r--chrome/browser/history/download_database.cc188
-rw-r--r--chrome/browser/history/download_database.h91
-rw-r--r--chrome/browser/history/download_types.h89
-rw-r--r--chrome/browser/history/expire_history_backend.cc512
-rw-r--r--chrome/browser/history/expire_history_backend.h272
-rw-r--r--chrome/browser/history/expire_history_backend_unittest.cc710
-rw-r--r--chrome/browser/history/history.cc697
-rw-r--r--chrome/browser/history/history.h839
-rw-r--r--chrome/browser/history/history_backend.cc2060
-rw-r--r--chrome/browser/history/history_backend.h517
-rw-r--r--chrome/browser/history/history_backend_unittest.cc316
-rw-r--r--chrome/browser/history/history_database.cc248
-rw-r--r--chrome/browser/history/history_database.h192
-rw-r--r--chrome/browser/history/history_marshaling.h160
-rw-r--r--chrome/browser/history/history_notifications.h114
-rw-r--r--chrome/browser/history/history_querying_unittest.cc371
-rw-r--r--chrome/browser/history/history_types.cc260
-rw-r--r--chrome/browser/history/history_types.h564
-rw-r--r--chrome/browser/history/history_types_unittest.cc194
-rw-r--r--chrome/browser/history/history_unittest.cc925
-rw-r--r--chrome/browser/history/in_memory_database.cc127
-rw-r--r--chrome/browser/history/in_memory_database.h79
-rw-r--r--chrome/browser/history/in_memory_history_backend.cc184
-rw-r--r--chrome/browser/history/in_memory_history_backend.h107
-rw-r--r--chrome/browser/history/page_usage_data.cc38
-rw-r--r--chrome/browser/history/page_usage_data.h173
-rw-r--r--chrome/browser/history/query_parser.cc317
-rw-r--r--chrome/browser/history/query_parser.h97
-rw-r--r--chrome/browser/history/query_parser_unittest.cc137
-rw-r--r--chrome/browser/history/redirect_uitest.cc254
-rw-r--r--chrome/browser/history/snippet.cc297
-rw-r--r--chrome/browser/history/snippet.h91
-rw-r--r--chrome/browser/history/snippet_unittest.cc279
-rw-r--r--chrome/browser/history/starred_url_database.cc888
-rw-r--r--chrome/browser/history/starred_url_database.h289
-rw-r--r--chrome/browser/history/starred_url_database_unittest.cc674
-rw-r--r--chrome/browser/history/text_database.cc412
-rw-r--r--chrome/browser/history/text_database.h197
-rw-r--r--chrome/browser/history/text_database_manager.cc510
-rw-r--r--chrome/browser/history/text_database_manager.h325
-rw-r--r--chrome/browser/history/text_database_manager_unittest.cc485
-rw-r--r--chrome/browser/history/text_database_unittest.cc346
-rw-r--r--chrome/browser/history/thumbnail_database.cc475
-rw-r--r--chrome/browser/history/thumbnail_database.h189
-rw-r--r--chrome/browser/history/thumbnail_database_unittest.cc344
-rw-r--r--chrome/browser/history/url_database.cc485
-rw-r--r--chrome/browser/history/url_database.h326
-rw-r--r--chrome/browser/history/url_database_unittest.cc207
-rw-r--r--chrome/browser/history/visit_database.cc392
-rw-r--r--chrome/browser/history/visit_database.h171
-rw-r--r--chrome/browser/history/visit_database_unittest.cc265
-rw-r--r--chrome/browser/history/visit_tracker.cc131
-rw-r--r--chrome/browser/history/visit_tracker.h92
-rw-r--r--chrome/browser/history/visit_tracker_unittest.cc163
-rw-r--r--chrome/browser/history/visitsegment_database.cc413
-rw-r--r--chrome/browser/history/visitsegment_database.h112
58 files changed, 19604 insertions, 0 deletions
diff --git a/chrome/browser/history/archived_database.cc b/chrome/browser/history/archived_database.cc
new file mode 100644
index 0000000..19f6436
--- /dev/null
+++ b/chrome/browser/history/archived_database.cc
@@ -0,0 +1,126 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/string_util.h"
+#include "chrome/browser/history/archived_database.h"
+
+namespace history {
+
+namespace {
+
+static const int kDatabaseVersion = 1;
+
+} // namespace
+
+ArchivedDatabase::ArchivedDatabase()
+ : db_(NULL),
+ transaction_nesting_(0) {
+}
+
+ArchivedDatabase::~ArchivedDatabase() {
+}
+
+bool ArchivedDatabase::Init(const std::wstring& file_name) {
+ // Open the history database, using the narrow version of open indicates to
+ // sqlite that we want the database to be in UTF-8 if it doesn't already
+ // exist.
+ DCHECK(!db_) << "Already initialized!";
+ if (sqlite3_open(WideToUTF8(file_name).c_str(), &db_) != SQLITE_OK)
+ return false;
+ statement_cache_ = new SqliteStatementCache(db_);
+ DBCloseScoper scoper(&db_, &statement_cache_);
+
+ // Set the database page size to something a little larger to give us
+ // better performance (we're typically seek rather than bandwidth limited).
+ // This only has an effect before any tables have been created, otherwise
+ // this is a NOP. Must be a power of 2 and a max of 8192.
+ sqlite3_exec(db_, "PRAGMA page_size=4096", NULL, NULL, NULL);
+
+ // Don't use very much memory caching this database. We seldom use it for
+ // anything important.
+ sqlite3_exec(db_, "PRAGMA cache_size=64", NULL, NULL, NULL);
+
+ // Run the database in exclusive mode. Nobody else should be accessing the
+ // database while we're running, and this will give somewhat improved perf.
+ sqlite3_exec(db_, "PRAGMA locking_mode=EXCLUSIVE", NULL, NULL, NULL);
+
+ BeginTransaction();
+
+ // Version check.
+ if (!meta_table_.Init(std::string(), kDatabaseVersion, db_))
+ return false;
+ if (meta_table_.GetCompatibleVersionNumber() > kDatabaseVersion) {
+ // We ignore this error and just run without the database. Normally, if
+ // the user is running two versions, the main history DB will give a
+ // warning about a version from the future.
+ LOG(WARNING) << "Archived database is a future version.";
+ return false;
+ }
+
+ // Create the tables.
+ if (!CreateURLTable(false) || !InitVisitTable() ||
+ !InitKeywordSearchTermsTable())
+ return false;
+ CreateMainURLIndex();
+
+ // Succeeded: keep the DB open by detaching the auto-closer.
+ scoper.Detach();
+ db_closer_.Attach(&db_, &statement_cache_);
+ CommitTransaction();
+ return true;
+}
+
+void ArchivedDatabase::BeginTransaction() {
+ DCHECK(db_);
+ if (transaction_nesting_ == 0) {
+ int rv = sqlite3_exec(db_, "BEGIN TRANSACTION", NULL, NULL, NULL);
+ DCHECK(rv == SQLITE_OK) << "Failed to begin transaction";
+ }
+ transaction_nesting_++;
+}
+
+void ArchivedDatabase::CommitTransaction() {
+ DCHECK(db_);
+ DCHECK(transaction_nesting_ > 0) << "Committing too many transactions";
+ transaction_nesting_--;
+ if (transaction_nesting_ == 0) {
+ int rv = sqlite3_exec(db_, "COMMIT", NULL, NULL, NULL);
+ DCHECK(rv == SQLITE_OK) << "Failed to commit transaction";
+ }
+}
+
+sqlite3* ArchivedDatabase::GetDB() {
+ return db_;
+}
+
+SqliteStatementCache& ArchivedDatabase::GetStatementCache() {
+ return *statement_cache_;
+}
+
+} // namespace history
diff --git a/chrome/browser/history/archived_database.h b/chrome/browser/history/archived_database.h
new file mode 100644
index 0000000..985dd35
--- /dev/null
+++ b/chrome/browser/history/archived_database.h
@@ -0,0 +1,88 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_ARCHIVED_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_ARCHIVED_DATABASE_H__
+
+#include "base/basictypes.h"
+#include "chrome/browser/history/download_database.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/browser/history/visit_database.h"
+#include "chrome/browser/meta_table_helper.h"
+
+struct sqlite3;
+
+namespace history {
+
+// Encapsulates the database operations for archived history.
+//
+// IMPORTANT NOTE: The IDs in this system for URLs and visits will be
+// different than those in the main database. This is to eliminate the
+// dependency between them so we can deal with each one on its own.
+class ArchivedDatabase : public URLDatabase,
+ public VisitDatabase {
+ public:
+ // Must call Init() before using other members.
+ ArchivedDatabase();
+ virtual ~ArchivedDatabase();
+
+ // Initializes the database connection. This must return true before any other
+ // functions on this class are called.
+ bool Init(const std::wstring& file_name);
+
+ // Transactions on the database. We support nested transactions and only
+ // commit when the outermost one is committed (sqlite doesn't support true
+ // nested transactions).
+ void BeginTransaction();
+ void CommitTransaction();
+
+ private:
+ // Implemented for the specialized databases.
+ virtual sqlite3* GetDB();
+ virtual SqliteStatementCache& GetStatementCache();
+
+ // The database.
+ //
+ // The close scoper will free the database and delete the statement cache in
+ // the correct order automatically when we are destroyed.
+ DBCloseScoper db_closer_;
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+
+ // The number of nested transactions currently in progress.
+ int transaction_nesting_;
+
+ MetaTableHelper meta_table_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(ArchivedDatabase);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_ARCHIVED_DATABASE_H__
diff --git a/chrome/browser/history/download_database.cc b/chrome/browser/history/download_database.cc
new file mode 100644
index 0000000..13216f8
--- /dev/null
+++ b/chrome/browser/history/download_database.cc
@@ -0,0 +1,188 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <limits>
+#include <vector>
+
+#include "chrome/browser/history/download_database.h"
+
+#include "chrome/browser/download_manager.h"
+#include "chrome/browser/history/download_types.h"
+#include "chrome/common/sqlite_utils.h"
+#include "chrome/common/sqlite_compiled_statement.h"
+
+// Download schema:
+//
+// id SQLite-generated primary key.
+// full_path Location of the download on disk.
+// url URL of the downloaded file.
+// start_time When the download was started.
+// received_bytes Total size downloaded.
+// total_bytes Total size of the download.
+// state Identifies if this download is completed or not. Not used
+// directly by the history system. See DownloadItem's
+// DownloadState for where this is used.
+
+namespace history {
+
+DownloadDatabase::DownloadDatabase() {
+}
+
+DownloadDatabase::~DownloadDatabase() {
+}
+
+bool DownloadDatabase::InitDownloadTable() {
+ if (!DoesSqliteTableExist(GetDB(), "downloads")) {
+ if (sqlite3_exec(GetDB(),
+ "CREATE TABLE downloads ("
+ "id INTEGER PRIMARY KEY,"
+ "full_path LONGVARCHAR NOT NULL,"
+ "url LONGVARCHAR NOT NULL,"
+ "start_time INTEGER NOT NULL,"
+ "received_bytes INTEGER NOT NULL,"
+ "total_bytes INTEGER NOT NULL,"
+ "state INTEGER NOT NULL)", NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ }
+ return true;
+}
+
+bool DownloadDatabase::DropDownloadTable() {
+ return sqlite3_exec(GetDB(), "DROP TABLE downloads", NULL, NULL, NULL) ==
+ SQLITE_OK;
+}
+
+void DownloadDatabase::QueryDownloads(std::vector<DownloadCreateInfo>* results) {
+ results->clear();
+
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT id, full_path, url, start_time, received_bytes, "
+ "total_bytes, state "
+ "FROM downloads "
+ "ORDER BY start_time");
+ if (!statement.is_valid())
+ return;
+
+ while (statement->step() == SQLITE_ROW) {
+ DownloadCreateInfo info;
+ info.db_handle = statement->column_int64(0);
+ statement->column_string16(1, &info.path);
+ statement->column_string16(2, &info.url);
+ info.start_time = Time::FromTimeT(statement->column_int64(3));
+ info.received_bytes = statement->column_int64(4);
+ info.total_bytes = statement->column_int64(5);
+ info.state = statement->column_int(6);
+ results->push_back(info);
+ }
+}
+
+bool DownloadDatabase::UpdateDownload(int64 received_bytes,
+ int32 state,
+ DownloadID db_handle) {
+ DCHECK(db_handle > 0);
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "UPDATE downloads "
+ "SET received_bytes=?, state=? WHERE id=?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, received_bytes);
+ statement->bind_int(1, state);
+ statement->bind_int64(2, db_handle);
+ return statement->step() == SQLITE_DONE;
+}
+
+int64 DownloadDatabase::CreateDownload(const DownloadCreateInfo& info) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "INSERT INTO downloads "
+ "(full_path, url, start_time, received_bytes, total_bytes, state) "
+ "VALUES (?, ?, ?, ?, ?, ?)");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_wstring(0, info.path);
+ statement->bind_wstring(1, info.url);
+ statement->bind_int64(2, info.start_time.ToTimeT());
+ statement->bind_int64(3, info.received_bytes);
+ statement->bind_int64(4, info.total_bytes);
+ statement->bind_int(5, info.state);
+ if (statement->step() == SQLITE_DONE)
+ return sqlite3_last_insert_rowid(GetDB());
+
+ return 0;
+}
+
+void DownloadDatabase::RemoveDownload(DownloadID db_handle) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "DELETE FROM downloads WHERE id=?");
+ if (!statement.is_valid())
+ return;
+
+ statement->bind_int64(0, db_handle);
+ statement->step();
+}
+
+void DownloadDatabase::RemoveDownloadsBetween(Time delete_begin,
+ Time delete_end) {
+ // This does not use an index. We currently aren't likely to have enough
+ // downloads where an index by time will give us a lot of benefit.
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "DELETE FROM downloads WHERE start_time >= ? AND start_time < ? "
+ "AND (State = ? OR State = ?)");
+ if (!statement.is_valid())
+ return;
+
+ time_t start_time = delete_begin.ToTimeT();
+ time_t end_time = delete_end.ToTimeT();
+ statement->bind_int64(0, start_time);
+ statement->bind_int64(1, end_time ? end_time : std::numeric_limits<int64>::max());
+ statement->bind_int(2, DownloadItem::COMPLETE);
+ statement->bind_int(3, DownloadItem::CANCELLED);
+ statement->step();
+}
+
+void DownloadDatabase::SearchDownloads(std::vector<int64>* results,
+ const std::wstring& search_text) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT id FROM downloads WHERE url LIKE ? "
+ "OR full_path LIKE ? ORDER BY id");
+ if (!statement.is_valid())
+ return;
+
+ std::wstring text(L"%");
+ text.append(search_text);
+ text.append(L"%");
+ statement->bind_wstring(0, text);
+ statement->bind_wstring(1, text);
+
+ while (statement->step() == SQLITE_ROW)
+ results->push_back(statement->column_int64(0));
+}
+
+} // namespace history
diff --git a/chrome/browser/history/download_database.h b/chrome/browser/history/download_database.h
new file mode 100644
index 0000000..313e8ed
--- /dev/null
+++ b/chrome/browser/history/download_database.h
@@ -0,0 +1,91 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_DOWNLOAD_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_DOWNLOAD_DATABASE_H__
+
+#include "chrome/browser/history/history_types.h"
+
+struct sqlite3;
+class SqliteStatementCache;
+class SQLStatement;
+struct DownloadCreateInfo;
+
+namespace history {
+
+// Maintains a table of downloads.
+class DownloadDatabase {
+ public:
+ // Must call InitDownloadTable before using any other functions.
+ DownloadDatabase();
+ virtual ~DownloadDatabase();
+
+ // Get all the downloads from the database.
+ void QueryDownloads(std::vector<DownloadCreateInfo>* results);
+
+ // Update the state of one download. Returns true if successful.
+ bool UpdateDownload(int64 received_bytes, int32 state, DownloadID db_handle);
+
+ // Create a new database entry for one download and return its primary db id.
+ int64 CreateDownload(const DownloadCreateInfo& info);
+
+ // Remove a download from the database.
+ void RemoveDownload(DownloadID db_handle);
+
+ // Remove all completed downloads that started after |remove_begin|
+ // (inclusive) and before |remove_end|. You may use null Time values
+ // to do an unbounded delete in either direction. This function ignores
+ // all downloads that are in progress or are waiting to be cancelled.
+ void RemoveDownloadsBetween(Time remove_begin, Time remove_end);
+
+ // Search for downloads matching the search text.
+ void SearchDownloads(std::vector<int64>* results,
+ const std::wstring& search_text);
+
+ protected:
+ // Returns the database and statement cache for the functions in this
+ // interface. The descendant of this class implements these functions to
+ // return its objects.
+ virtual sqlite3* GetDB() = 0;
+ virtual SqliteStatementCache& GetStatementCache() = 0;
+
+ // Creates the downloads table if needed.
+ bool InitDownloadTable();
+
+ // Used to quickly clear the downloads. First you would drop it, then you
+ // would re-initialize it.
+ bool DropDownloadTable();
+
+ private:
+ DISALLOW_EVIL_CONSTRUCTORS(DownloadDatabase);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_DOWNLOAD_DATABASE_H__
diff --git a/chrome/browser/history/download_types.h b/chrome/browser/history/download_types.h
new file mode 100644
index 0000000..27def96
--- /dev/null
+++ b/chrome/browser/history/download_types.h
@@ -0,0 +1,89 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+//
+// Download creation struct used for querying the history service.
+
+#ifndef CHROME_BROWSER_DOWNLOAD_TYPES_H__
+#define CHROME_BROWSER_DOWNLOAD_TYPES_H__
+
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/time.h"
+
+// Used for informing the download database of a new download, where we don't
+// want to pass DownloadItems between threads. The history service also uses a
+// vector of these structs for passing us the state of all downloads at
+// initialization time (see DownloadQueryInfo below).
+struct DownloadCreateInfo {
+ DownloadCreateInfo(const std::wstring& path,
+ const std::wstring& url,
+ Time start_time,
+ int64 received_bytes,
+ int64 total_bytes,
+ int32 state,
+ int32 download_id)
+ : path(path),
+ url(url),
+ suggested_path_exists(false),
+ start_time(start_time),
+ received_bytes(received_bytes),
+ total_bytes(total_bytes),
+ state(state),
+ download_id(download_id),
+ render_process_id(-1),
+ render_view_id(-1),
+ request_id(-1),
+ db_handle(0),
+ save_as(false) {
+ }
+
+ DownloadCreateInfo() : download_id(-1) {}
+
+ // DownloadItem fields
+ std::wstring path;
+ std::wstring url;
+ std::wstring suggested_path;
+ bool suggested_path_exists;
+ Time start_time;
+ int64 received_bytes;
+ int64 total_bytes;
+ int32 state;
+ int32 download_id;
+ int render_process_id;
+ int render_view_id;
+ int request_id;
+ int64 db_handle;
+ std::string content_disposition;
+ std::string mime_type;
+ bool save_as;
+};
+
+#endif // CHROME_BROWSER_DOWNLOAD_TYPES_H__
diff --git a/chrome/browser/history/expire_history_backend.cc b/chrome/browser/history/expire_history_backend.cc
new file mode 100644
index 0000000..ba42e07
--- /dev/null
+++ b/chrome/browser/history/expire_history_backend.cc
@@ -0,0 +1,512 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/expire_history_backend.h"
+
+#include <algorithm>
+#include <limits>
+
+#include "base/file_util.h"
+#include "chrome/browser/history/archived_database.h"
+#include "chrome/browser/history/history_database.h"
+#include "chrome/browser/history/history_notifications.h"
+#include "chrome/browser/history/text_database_manager.h"
+#include "chrome/browser/history/thumbnail_database.h"
+#include "chrome/common/notification_types.h"
+
+namespace history {
+
+namespace {
+
+// Returns true if this visit is worth archiving. Otherwise, this visit is not
+// worth saving (for example, subframe navigations and redirects) and we can
+// just delete it when it gets old.
+bool ShouldArchiveVisit(const VisitRow& visit) {
+ int no_qualifier = PageTransition::StripQualifier(visit.transition);
+
+ // These types of transitions are always "important" and the user will want
+ // to see them.
+ if (no_qualifier == PageTransition::TYPED ||
+ no_qualifier == PageTransition::AUTO_BOOKMARK ||
+ no_qualifier == PageTransition::START_PAGE)
+ return true;
+
+ // Only archive these "less important" transitions when they were the final
+ // navigation and not part of a redirect chain.
+ if ((no_qualifier == PageTransition::LINK ||
+ no_qualifier == PageTransition::FORM_SUBMIT ||
+ no_qualifier == PageTransition::GENERATED) &&
+ visit.transition & PageTransition::CHAIN_END)
+ return true;
+
+ // The transition types we ignore are AUTO_SUBFRAME and MANUAL_SUBFRAME.
+ return false;
+}
+
+// The number of visits we will expire very time we check for old items. This
+// Prevents us from doing too much work any given time.
+const int kNumExpirePerIteration = 10;
+
+// The number of seconds between checking for items that should be expired when
+// we think there might be more items to expire. This timeout is used when the
+// last expiration found at least kNumExpirePerIteration and we want to check
+// again "soon."
+const int kExpirationDelaySec = 60;
+
+// The number of minutes between checking, as with kExpirationDelaySec, but
+// when we didn't find enough things to expire last time. If there was no
+// history to expire last iteration, it's likely there is nothing next
+// iteration, so we want to wait longer before checking to avoid wasting CPU.
+const int kExpirationEmptyDelayMin = 5;
+
+} // namespace
+
+ExpireHistoryBackend::ExpireHistoryBackend(
+ BroadcastNotificationDelegate* delegate)
+ : delegate_(delegate),
+ main_db_(NULL),
+ archived_db_(NULL),
+ thumb_db_(NULL),
+ text_db_(NULL),
+#pragma warning(suppress: 4355) // Okay to pass "this" here.
+ factory_(this) {
+}
+
+ExpireHistoryBackend::~ExpireHistoryBackend() {
+}
+
+void ExpireHistoryBackend::SetDatabases(HistoryDatabase* main_db,
+ ArchivedDatabase* archived_db,
+ ThumbnailDatabase* thumb_db,
+ TextDatabaseManager* text_db) {
+ main_db_ = main_db;
+ archived_db_ = archived_db;
+ thumb_db_ = thumb_db;
+ text_db_ = text_db;
+}
+
+void ExpireHistoryBackend::DeleteURL(const GURL& url) {
+ if (!main_db_)
+ return;
+
+ URLRow url_row;
+ if (!main_db_->GetRowForURL(url, &url_row))
+ return; // Nothing to delete.
+
+ // The URL may be in the text database manager's temporary cache.
+ if (text_db_)
+ text_db_->DeleteURLFromUncommitted(url);
+
+ // Collect all the visits and delete them. Note that we don't give up if
+ // there are no visits, since the URL could still have an entry (for example,
+ // if it's starred) that we should delete.
+ // TODO(brettw): bug 1171148: We should also delete from the archived DB.
+ VisitVector visits;
+ main_db_->GetVisitsForURL(url_row.id(), &visits);
+
+ DeleteDependencies dependencies;
+ DeleteVisitRelatedInfo(visits, &dependencies);
+
+ // We skip ExpireURLsForVisits (since we are deleting from the URL, and not
+ // starting with visits in a given time range). We therefore need to call the
+ // deletion and favicon update functions manually.
+ DeleteOneURL(url_row, &dependencies);
+ DeleteFaviconsIfPossible(dependencies.affected_favicons);
+
+ if (text_db_)
+ text_db_->OptimizeChangedDatabases(dependencies.text_db_changes);
+ BroadcastDeleteNotifications(&dependencies);
+}
+
+void ExpireHistoryBackend::ExpireHistoryBetween(Time begin_time,
+ Time end_time) {
+ if (!main_db_)
+ return;
+
+ // There may be stuff in the text database manager's temporary cache.
+ if (text_db_)
+ text_db_->DeleteFromUncommitted(begin_time, end_time);
+
+ // Find the affected visits and delete them.
+ // TODO(brettw): bug 1171164: We should query the archived database here, too.
+ VisitVector visits;
+ main_db_->GetAllVisitsInRange(begin_time, end_time, 0, &visits);
+ if (visits.empty())
+ return;
+
+ DeleteDependencies dependencies;
+ DeleteVisitRelatedInfo(visits, &dependencies);
+
+ // Delete or update the URLs affected. We want to update the visit counts
+ // since this is called by the user who wants to delete their recent history,
+ // and we don't want to leave any evidence.
+ ExpireURLsForVisits(visits, &dependencies);
+ DeleteFaviconsIfPossible(dependencies.affected_favicons);
+
+ // An is_null begin time means that all history should be deleted.
+ BroadcastDeleteNotifications(&dependencies);
+
+ // Pick up any bits possibly left over.
+ ParanoidExpireHistory();
+}
+
+void ExpireHistoryBackend::ArchiveHistoryBefore(Time end_time) {
+ if (!main_db_)
+ return;
+
+ // Archive as much history as possible before the given date.
+ ArchiveSomeOldHistory(end_time, std::numeric_limits<size_t>::max());
+ ParanoidExpireHistory();
+}
+
+void ExpireHistoryBackend::StartArchivingOldStuff(
+ TimeDelta expiration_threshold) {
+ expiration_threshold_ = expiration_threshold;
+ ScheduleArchive(TimeDelta::FromSeconds(kExpirationDelaySec));
+}
+
+void ExpireHistoryBackend::DeleteFaviconsIfPossible(
+ const std::set<FavIconID>& favicon_set) {
+ if (!main_db_ || !thumb_db_)
+ return;
+
+ for (std::set<FavIconID>::const_iterator i = favicon_set.begin();
+ i != favicon_set.end(); ++i) {
+ if (!main_db_->IsFavIconUsed(*i))
+ thumb_db_->DeleteFavIcon(*i);
+ }
+}
+
+void ExpireHistoryBackend::BroadcastDeleteNotifications(
+ DeleteDependencies* dependencies) {
+ // Broadcast the "unstarred" notification.
+ if (!dependencies->unstarred_urls.empty()) {
+ URLsStarredDetails* unstarred_details = new URLsStarredDetails(false);
+ unstarred_details->changed_urls.swap(dependencies->unstarred_urls);
+ unstarred_details->star_entries.swap(dependencies->unstarred_entries);
+ delegate_->BroadcastNotifications(NOTIFY_URLS_STARRED, unstarred_details);
+ }
+
+ if (!dependencies->deleted_urls.empty()) {
+ // Broadcast the URL deleted notification. Note that we also broadcast when
+ // we were requested to delete everything even if that was a NOP, since
+ // some components care to know when history is deleted (it's up to them to
+ // determine if they care whether anything was deleted).
+ URLsDeletedDetails* deleted_details = new URLsDeletedDetails;
+ deleted_details->all_history = false;
+ std::vector<URLRow> typed_urls_changed; // Collect this for later.
+ for (size_t i = 0; i < dependencies->deleted_urls.size(); i++) {
+ deleted_details->urls.insert(dependencies->deleted_urls[i].url());
+ if (dependencies->deleted_urls[i].typed_count() > 0)
+ typed_urls_changed.push_back(dependencies->deleted_urls[i]);
+ }
+ delegate_->BroadcastNotifications(NOTIFY_HISTORY_URLS_DELETED,
+ deleted_details);
+
+ // Broadcast the typed URL changed modification (this updates the inline
+ // autocomplete database).
+ //
+ // Note: if we ever need to broadcast changes to more than just typed URLs,
+ // this notification should be changed rather than a new "non-typed"
+ // notification added. The in-memory database can always do the filtering
+ // itself in that case.
+ if (!typed_urls_changed.empty()) {
+ URLsModifiedDetails* modified_details = new URLsModifiedDetails;
+ modified_details->changed_urls.swap(typed_urls_changed);
+ delegate_->BroadcastNotifications(NOTIFY_HISTORY_TYPED_URLS_MODIFIED,
+ modified_details);
+ }
+ }
+}
+
+void ExpireHistoryBackend::DeleteVisitRelatedInfo(
+ const VisitVector& visits,
+ DeleteDependencies* dependencies) {
+ for (size_t i = 0; i < visits.size(); i++) {
+ // Delete the visit itself.
+ main_db_->DeleteVisit(visits[i]);
+
+ // Add the URL row to the affected URL list.
+ std::map<URLID, URLRow>::const_iterator found =
+ dependencies->affected_urls.find(visits[i].url_id);
+ const URLRow* cur_row;
+ if (found == dependencies->affected_urls.end()) {
+ URLRow row;
+ if (!main_db_->GetURLRow(visits[i].url_id, &row))
+ continue;
+ dependencies->affected_urls[visits[i].url_id] = row;
+ cur_row = &dependencies->affected_urls[visits[i].url_id];
+ } else {
+ cur_row = &found->second;
+ }
+
+ // Delete any associated full-text indexed data.
+ if (visits[i].is_indexed && text_db_) {
+ text_db_->DeletePageData(visits[i].visit_time, cur_row->url(),
+ &dependencies->text_db_changes);
+ }
+ }
+}
+
+void ExpireHistoryBackend::DeleteOneURL(
+ const URLRow& url_row,
+ DeleteDependencies* dependencies) {
+ dependencies->deleted_urls.push_back(url_row);
+
+ // Delete stuff that references this URL.
+ if (thumb_db_)
+ thumb_db_->DeleteThumbnail(url_row.id());
+ main_db_->DeleteSegmentForURL(url_row.id());
+
+ // The starred table may need to have its corresponding item deleted.
+ if (url_row.star_id()) {
+ // Note that this will update the URLRow's starred field, but our variable
+ // won't be updated correspondingly.
+ main_db_->DeleteStarredEntry(url_row.star_id(),
+ &dependencies->unstarred_urls,
+ &dependencies->unstarred_entries);
+ }
+
+ // Collect shared information.
+ if (url_row.favicon_id())
+ dependencies->affected_favicons.insert(url_row.favicon_id());
+
+ // Last, delete the URL entry.
+ main_db_->DeleteURLRow(url_row.id());
+}
+
+URLID ExpireHistoryBackend::ArchiveOneURL(const URLRow& url_row) {
+ if (!archived_db_)
+ return 0;
+
+ // See if this URL is present in the archived database already. Note that
+ // we must look up by ID since the URL ID will be different.
+ URLRow archived_row;
+ if (archived_db_->GetRowForURL(url_row.url(), &archived_row)) {
+ // TODO(sky): bug 1168470, need to archive past search terms.
+ // FIXME(brettw) should be copy the visit counts over? This will mean that
+ // the main DB's visit counts are only for the last 3 months rather than
+ // accumulative.
+ archived_row.set_last_visit(url_row.last_visit());
+ archived_db_->UpdateURLRow(archived_row.id(), archived_row);
+ return archived_row.id();
+ }
+
+ // This row is not in the archived DB, add it.
+ return archived_db_->AddURL(url_row);
+}
+
+void ExpireHistoryBackend::ExpireURLsForVisits(
+ const VisitVector& visits,
+ DeleteDependencies* dependencies) {
+ // First find all unique URLs and the number of visits we're deleting for
+ // each one.
+ struct ChangedURL {
+ ChangedURL() : visit_count(0), typed_count(0) {}
+ int visit_count;
+ int typed_count;
+ };
+ std::map<URLID, ChangedURL> changed_urls;
+ for (size_t i = 0; i < visits.size(); i++) {
+ ChangedURL& cur = changed_urls[visits[i].url_id];
+ cur.visit_count++;
+ // NOTE: This code must stay in sync with HistoryBackend::AddPageVisit().
+ // TODO(pkasting): http://b/1148304 We shouldn't be marking so many URLs as
+ // typed, which would eliminate the need for this code.
+ const PageTransition::Type transition = visits[i].transition;
+ if (PageTransition::StripQualifier(transition) == PageTransition::TYPED &&
+ !PageTransition::IsRedirect(transition))
+ cur.typed_count++;
+ }
+
+ // Check each unique URL with deleted visits.
+ for (std::map<URLID, ChangedURL>::const_iterator i = changed_urls.begin();
+ i != changed_urls.end(); ++i) {
+ // The unique URL rows should already be filled into the dependencies.
+ URLRow& url_row = dependencies->affected_urls[i->first];
+ if (!url_row.id())
+ continue; // URL row doesn't exist in the database.
+
+ // Check if there are any other visits for this URL and update the time
+ // (the time change may not actually be synced to disk below when we're
+ // archiving).
+ VisitRow last_visit;
+ if (main_db_->GetMostRecentVisitForURL(url_row.id(), &last_visit))
+ url_row.set_last_visit(last_visit.visit_time);
+ else
+ url_row.set_last_visit(Time());
+
+ // Don't delete starred URLs or ones with visits still in the DB.
+ if (url_row.starred() || !url_row.last_visit().is_null()) {
+ // We're not deleting the URL, update its counts when we're deleting those
+ // visits.
+ // NOTE: The calls to std::max() below are a backstop, but they should
+ // never actually be needed unless the database is corrupt (I think).
+ url_row.set_visit_count(
+ std::max(0, url_row.visit_count() - i->second.visit_count));
+ url_row.set_typed_count(
+ std::max(0, url_row.typed_count() - i->second.typed_count));
+ main_db_->UpdateURLRow(url_row.id(), url_row);
+ } else {
+ // This URL is toast.
+ DeleteOneURL(url_row, dependencies);
+ }
+ }
+}
+
+void ExpireHistoryBackend::ArchiveURLsAndVisits(
+ const VisitVector& visits,
+ DeleteDependencies* dependencies) {
+ // Make sure all unique URL rows are added to the dependency list and the
+ // archived database. We will also keep the mapping between the main DB URLID
+ // and the archived one.
+ std::map<URLID, URLID> main_id_to_archived_id;
+ for (size_t i = 0; i < visits.size(); i++) {
+ std::map<URLID, URLRow>::const_iterator found =
+ dependencies->affected_urls.find(visits[i].url_id);
+ if (found == dependencies->affected_urls.end()) {
+ // Unique URL encountered, archive it.
+ URLRow row; // Row in the main DB.
+ URLID archived_id; // ID in the archived DB.
+ if (!main_db_->GetURLRow(visits[i].url_id, &row) ||
+ !(archived_id = ArchiveOneURL(row))) {
+ // Failure archiving, skip this one.
+ continue;
+ }
+
+ // Only add URL to the dependency list once we know we successfully
+ // archived it.
+ main_id_to_archived_id[row.id()] = archived_id;
+ dependencies->affected_urls[row.id()] = row;
+ }
+ }
+
+ // Now archive the visits since we know the URL ID to make them reference.
+ // The source visit list should still reference the visits in the main DB, but
+ // we will update it to reflect only the visits that were successfully
+ // archived.
+ for (size_t i = 0; i < visits.size(); i++) {
+ // Construct the visit that we will add to the archived database. We do
+ // not store referring visits since we delete many of the visits when
+ // archiving.
+ VisitRow cur_visit(visits[i]);
+ cur_visit.url_id = main_id_to_archived_id[cur_visit.url_id];
+ cur_visit.referring_visit = 0;
+ archived_db_->AddVisit(&cur_visit);
+ // Ignore failures, we will delete it from the main DB no matter what.
+ }
+}
+
+void ExpireHistoryBackend::ScheduleArchive(TimeDelta delay) {
+ factory_.RevokeAll();
+ MessageLoop::current()->PostDelayedTask(FROM_HERE, factory_.NewRunnableMethod(
+ &ExpireHistoryBackend::DoArchiveIteration),
+ static_cast<int>(delay.InMilliseconds()));
+}
+
+void ExpireHistoryBackend::DoArchiveIteration() {
+ DCHECK(expiration_threshold_ != TimeDelta()) << "threshold should be set";
+ Time threshold = Time::Now() - expiration_threshold_;
+
+ if (ArchiveSomeOldHistory(threshold, kNumExpirePerIteration)) {
+ // Possibly more items to delete now, schedule it sooner to happen again.
+ ScheduleArchive(TimeDelta::FromSeconds(kExpirationDelaySec));
+ } else {
+ // If we didn't find the maximum number of items to delete, wait longer
+ // before trying to delete more later.
+ ScheduleArchive(TimeDelta::FromMinutes(kExpirationEmptyDelayMin));
+ }
+}
+
+bool ExpireHistoryBackend::ArchiveSomeOldHistory(Time time_threshold,
+ int max_visits) {
+ if (!main_db_)
+ return false;
+
+ // Get all visits up to and including the threshold. This is a little tricky
+ // because GetAllVisitsInRange's end value is non-inclusive, so we have to
+ // increment the time by one unit to get the input value to be inclusive.
+ DCHECK(!time_threshold.is_null());
+ Time effective_threshold =
+ Time::FromInternalValue(time_threshold.ToInternalValue() + 1);
+ VisitVector affected_visits;
+ main_db_->GetAllVisitsInRange(Time(), effective_threshold, max_visits,
+ &affected_visits);
+
+ // Some visits we'll delete while others we'll archive.
+ VisitVector deleted_visits, archived_visits;
+ for (size_t i = 0; i < affected_visits.size(); i++) {
+ if (ShouldArchiveVisit(affected_visits[i]))
+ archived_visits.push_back(affected_visits[i]);
+ else
+ deleted_visits.push_back(affected_visits[i]);
+ }
+
+ // Do the actual archiving.
+ DeleteDependencies archived_dependencies;
+ ArchiveURLsAndVisits(archived_visits, &archived_dependencies);
+ DeleteVisitRelatedInfo(archived_visits, &archived_dependencies);
+
+ DeleteDependencies deleted_dependencies;
+ DeleteVisitRelatedInfo(deleted_visits, &deleted_dependencies);
+
+ // This will remove or archive all the affected URLs. Must do the deleting
+ // cleanup before archiving so the delete dependencies structure references
+ // only those URLs that were actually deleted instead of having some visits
+ // archived and then the rest deleted.
+ ExpireURLsForVisits(deleted_visits, &deleted_dependencies);
+ ExpireURLsForVisits(archived_visits, &archived_dependencies);
+
+ // Create a union of all affected favicons (we don't store favicons for
+ // archived URLs) and delete them.
+ std::set<FavIconID> affected_favicons(
+ archived_dependencies.affected_favicons);
+ for (std::set<FavIconID>::const_iterator i =
+ deleted_dependencies.affected_favicons.begin();
+ i != deleted_dependencies.affected_favicons.end(); ++i) {
+ affected_favicons.insert(*i);
+ }
+ DeleteFaviconsIfPossible(affected_favicons);
+
+ // Send notifications for the stuff that was deleted. These won't normally be
+ // in history views since they were subframes, but they will be in the visited
+ // link system, which needs to be updated now. This function is smart enough
+ // to not do anything if nothing was deleted.
+ BroadcastDeleteNotifications(&deleted_dependencies);
+
+ // When we got the maximum number of visits we asked for, we say there could
+ // be additional things to expire now.
+ return static_cast<int>(affected_visits.size()) == max_visits;
+}
+
+void ExpireHistoryBackend::ParanoidExpireHistory() {
+ // FIXME(brettw): Bug 1067331: write this to clean up any errors.
+}
+
+} // namespace history
diff --git a/chrome/browser/history/expire_history_backend.h b/chrome/browser/history/expire_history_backend.h
new file mode 100644
index 0000000..4862f9a
--- /dev/null
+++ b/chrome/browser/history/expire_history_backend.h
@@ -0,0 +1,272 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_EXPIRE_HISTORY_BACKEND_H__
+#define CHROME_BROWSER_HISTORY_EXPIRE_HISTORY_BACKEND_H__
+
+#include <set>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/task.h"
+#include "base/time.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/history/text_database_manager.h"
+#include "testing/gtest/include/gtest/gtest_prod.h"
+
+class GURL;
+enum NotificationType;
+
+namespace history {
+
+class ArchivedDatabase;
+class HistoryDatabase;
+struct HistoryDetails;
+class ThumbnailDatabase;
+
+// Delegate used to broadcast notifications to the main thread.
+class BroadcastNotificationDelegate {
+ public:
+ // Schedules a broadcast of the given notification on the application main
+ // thread. The details argument will have ownership taken by this function.
+ virtual void BroadcastNotifications(NotificationType type,
+ HistoryDetails* details_deleted) = 0;
+};
+
+// Helper component to HistoryBackend that manages expiration and deleting of
+// history, as well as moving data from the main database to the archived
+// database as it gets old.
+//
+// It will automatically start periodically archiving old history once you call
+// StartArchivingOldStuff().
+class ExpireHistoryBackend {
+ public:
+ // The delegate pointer must be non-NULL. We will NOT take ownership of it.
+ explicit ExpireHistoryBackend(BroadcastNotificationDelegate* delegate);
+ ~ExpireHistoryBackend();
+
+ // Completes initialization by setting the databases that this class will use.
+ void SetDatabases(HistoryDatabase* main_db,
+ ArchivedDatabase* archived_db,
+ ThumbnailDatabase* thumb_db,
+ TextDatabaseManager* text_db);
+
+ // Begins periodic expiration of history older than the given threshold. This
+ // will continue until the object is deleted.
+ void StartArchivingOldStuff(TimeDelta expiration_threshold);
+
+ // Deletes everything associated with a URL, regardless of whether it is
+ // starred or not.
+ void DeleteURL(const GURL& url);
+
+ // Removes all visits in the given time range, updating the URLs accordingly.
+ void ExpireHistoryBetween(Time begin_time, Time end_time);
+
+ // Archives all visits before and including the given time, updating the URLs
+ // accordingly. This function is intended for migrating old databases
+ // (which encompased all time) to the tiered structure and testing, and
+ // probably isn't useful for anything else.
+ void ArchiveHistoryBefore(Time end_time);
+
+ // Returns the current time that we are archiving stuff to. This will return
+ // the threshold in absolute time rather than a delta, so the caller should
+ // not save it.
+ Time GetCurrentArchiveTime() const {
+ return Time::Now() - expiration_threshold_;
+ }
+
+ private:
+ //friend class ExpireHistoryTest_DeleteFaviconsIfPossible_Test;
+ FRIEND_TEST(ExpireHistoryTest, DeleteTextIndexForURL);
+ FRIEND_TEST(ExpireHistoryTest, DeleteFaviconsIfPossible);
+ FRIEND_TEST(ExpireHistoryTest, ArchiveSomeOldHistory);
+
+ struct DeleteDependencies {
+ // The time range affected. These can be is_null() to be unbounded in one
+ // or both directions.
+ Time begin_time, end_time;
+
+ // ----- Filled by DeleteVisitRelatedInfo or manually if a function doesn't
+ // call that function. -----
+
+ // The unique URL rows affected by this delete.
+ std::map<URLID, URLRow> affected_urls;
+
+ // ----- Filled by DeleteOneURL -----
+
+ // The URLs deleted during this operation.
+ std::vector<URLRow> deleted_urls;
+
+ // The list of all favicon IDs that the affected URLs had. Favicons will be
+ // shared between all URLs with the same favicon, so this is the set of IDs
+ // that we will need to check when the delete operations are complete.
+ std::set<FavIconID> affected_favicons;
+
+ // URLs that were unstarred as a result of the delete.
+ std::set<GURL> unstarred_urls;
+ std::vector<StarredEntry> unstarred_entries;
+
+ // Tracks the set of databases that have changed so we can optimize when
+ // when we're done.
+ TextDatabaseManager::ChangeSet text_db_changes;
+ };
+
+ // Removes the data from the full text index associated with the given URL
+ // string/ID pair. If |update_visits| is set, the visits that reference the
+ // indexed data will be updated to reflect the fact that the indexed data is
+ // gone. Setting this to false is a performance optimization when the caller
+ // knows that the visits will be deleted after the call.
+ //
+ // TODO(brettw) when we have an "archived" history database, this should take
+ // a flag to optionally delete from there. This way it can be used for page
+ // re-indexing as well as for full URL deletion.
+ void DeleteTextIndexForURL(const GURL& url, URLID url_id, bool update_visits);
+
+ // Deletes the visit-related stuff for all the visits in the given list, and
+ // adds the rows for unique URLs affected to the affected_urls list in
+ // the dependencies structure.
+ //
+ // Deleted information is the visits themselves and the full-text index
+ // entries corresponding to them.
+ void DeleteVisitRelatedInfo(const VisitVector& visits,
+ DeleteDependencies* dependencies);
+
+ // Moves the given visits from the main database to the archived one.
+ void ArchiveVisits(const VisitVector& visits);
+
+ // Finds or deletes dependency information for the given URL. Information that
+ // is specific to this URL (URL row, thumbnails, full text indexed stuff,
+ // etc.) is deleted.
+ //
+ // This does not affect the visits! This is used for expiration as well as
+ // deleting from the UI, and they handle visits differently.
+ //
+ // Other information will be collected and returned in the output containers.
+ // This includes some of the things deleted that are needed elsewhere, plus
+ // some things like favicons that could be shared by many URLs, and need to
+ // be checked for deletion (this allows us to delete many URLs with only one
+ // check for shared information at the end).
+ //
+ // Assumes the main_db_ is non-NULL.
+ void DeleteOneURL(const URLRow& url_row, DeleteDependencies* dependencies);
+
+ // Adds or merges the given URL row with the archived database, returning the
+ // ID of the URL in the archived database, or 0 on failure. The main (source)
+ // database will not be affected (the URL will have to be deleted later).
+ //
+ // Assumes the archived database is not NULL.
+ URLID ArchiveOneURL(const URLRow& url_row);
+
+ // Deletes all the URLs in the given vector and handles their dependencies.
+ // This will delete starred URLs
+ void DeleteURLs(const std::vector<URLRow>& urls,
+ DeleteDependencies* dependencies);
+
+ // Expiration involves removing visits, then propogating the visits out from
+ // there and delete any orphaned URLs. These will be added to the deleted URLs
+ // field of the dependencies and DeleteOneURL will handle deleting out from
+ // there. This function does not handle favicons.
+ //
+ // When a URL is not deleted and |archive| is not set, the last visit time and
+ // the visit and typed counts will be updated (we want to clear these when a
+ // user is deleting history manually, but not when we're normally expiring old
+ // things from history).
+ //
+ // The visits in the given vector should have already been deleted from the
+ // database, and the list of affected URLs already be filled into
+ // |depenencies->affected_urls|.
+ //
+ // Starred URLs will not be deleted. The information in the dependencies that
+ // DeleteOneURL fills in will be updated, and this function will also delete
+ // any now-unused favicons.
+ void ExpireURLsForVisits(const VisitVector& visits,
+ DeleteDependencies* dependencies);
+
+ // Creates entries in the archived database for the unique URLs referenced
+ // by the given visits. It will then add versions of the visits to that
+ // database. The source database WILL NOT BE MODIFIED. The source URLs and
+ // visits will have to be deleted in another pass.
+ //
+ // The affected URLs will be filled into the given dependencies structure.
+ void ArchiveURLsAndVisits(const VisitVector& visits,
+ DeleteDependencies* dependencies);
+
+ // Deletes the favicons listed in the set if unused. Fails silently (we don't
+ // care about favicons so much, so don't want to stop everything if it fails).
+ void DeleteFaviconsIfPossible(const std::set<FavIconID>& favicon_id);
+
+ // Broadcasts that the given URLs and star entries were deleted. Either list
+ // can be empty, in which case no notification will be sent for that type.
+ //
+ // The passed-in arguments will be cleared becuase they will be swapped into
+ // the destination message to avoid copying). However, ownership of the
+ // dependencies object will not transfer.
+ void BroadcastDeleteNotifications(DeleteDependencies* dependencies);
+
+ // Schedules a call to DoArchiveIteration at the given time in the
+ // future.
+ void ScheduleArchive(TimeDelta delay);
+
+ // Calls ArchiveSomeOldHistory to expire some amount of old history, and
+ // schedules another call to happen in the future.
+ void DoArchiveIteration();
+
+ // Tries to expire the oldest |max_visits| visits from history that are older
+ // than |time_threshold|. The return value indicates if we think there might
+ // be more history to expire with the current time threshold (it does not
+ // indicate success or failure).
+ bool ArchiveSomeOldHistory(Time time_threshold, int max_visits);
+
+ // Tries to detect possible bad history or inconsistencies in the database
+ // and deletes items. For example, URLs with no visits.
+ void ParanoidExpireHistory();
+
+ // Non-owning pointer to the notification delegate (guaranteed non-NULL).
+ BroadcastNotificationDelegate* delegate_;
+
+ // Non-owning pointers to the databases we deal with (MAY BE NULL).
+ HistoryDatabase* main_db_; // Main history database.
+ ArchivedDatabase* archived_db_; // Old history.
+ ThumbnailDatabase* thumb_db_; // Thumbnails and favicons.
+ TextDatabaseManager* text_db_; // Full text index.
+
+ // Used to generate runnable methods to do timers on this class. They will be
+ // automatically canceled when this class is deleted.
+ ScopedRunnableMethodFactory<ExpireHistoryBackend> factory_;
+
+ // The threshold for "old" history where we will automatically expire it to
+ // the archived database.
+ TimeDelta expiration_threshold_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(ExpireHistoryBackend);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_EXPIRE_HISTORY_BACKEND_H__
diff --git a/chrome/browser/history/expire_history_backend_unittest.cc b/chrome/browser/history/expire_history_backend_unittest.cc
new file mode 100644
index 0000000..0cd12da
--- /dev/null
+++ b/chrome/browser/history/expire_history_backend_unittest.cc
@@ -0,0 +1,710 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/basictypes.h"
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/history/archived_database.h"
+#include "chrome/browser/history/expire_history_backend.h"
+#include "chrome/browser/history/history_database.h"
+#include "chrome/browser/history/text_database_manager.h"
+#include "chrome/browser/history/thumbnail_database.h"
+#include "chrome/common/jpeg_codec.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/thumbnail_score.h"
+#include "chrome/tools/profiles/thumbnail-inl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "SkBitmap.h"
+
+// The test must be in the history namespace for the gtest forward declarations
+// to work. It also eliminates a bunch of ugly "history::".
+namespace history {
+
+// ExpireHistoryTest -----------------------------------------------------------
+
+class ExpireHistoryTest : public testing::Test,
+ public BroadcastNotificationDelegate {
+ public:
+#pragma warning(suppress: 4355) // OK to pass "this" here.
+ ExpireHistoryTest() : expirer_(this), now_(Time::Now()) {
+ }
+
+ protected:
+ // Called by individual tests when they want data populated.
+ void AddExampleData(URLID url_ids[3], Time visit_times[4]);
+
+ // Returns true if the given favicon/thumanil has an entry in the DB.
+ bool HasFavIcon(FavIconID favicon_id);
+ bool HasThumbnail(URLID url_id);
+
+ // Returns the number of text matches for the given URL in the example data
+ // added by AddExampleData.
+ int CountTextMatchesForURL(const GURL& url);
+
+ // EXPECTs that each URL-specific history thing (basically, everything but
+ // favicons) is gone.
+ void EnsureURLInfoGone(const URLRow& row);
+
+ // Clears the list of notifications received.
+ void ClearLastNotifications() {
+ for (size_t i = 0; i < notifications_.size(); i++)
+ delete notifications_[i].second;
+ notifications_.clear();
+ }
+
+ static bool IsStringInFile(std::wstring& filename, const char* str);
+
+ ExpireHistoryBackend expirer_;
+
+ scoped_ptr<HistoryDatabase> main_db_;
+ scoped_ptr<ArchivedDatabase> archived_db_;
+ scoped_ptr<ThumbnailDatabase> thumb_db_;
+ scoped_ptr<TextDatabaseManager> text_db_;
+
+ // Time at the beginning of the test, so everybody agrees what "now" is.
+ const Time now_;
+
+ // Notifications intended to be broadcast, we can check these values to make
+ // sure that the deletor is doing the correct broadcasts. We own the details
+ // pointers.
+ typedef std::vector< std::pair<NotificationType, HistoryDetails*> >
+ NotificationList;
+ NotificationList notifications_;
+
+ // Directory for the history files.
+ std::wstring dir_;
+
+ private:
+ void SetUp() {
+ PathService::Get(base::DIR_TEMP, &dir_);
+ file_util::AppendToPath(&dir_, L"ExpireTest");
+ file_util::Delete(dir_, true);
+ file_util::CreateDirectory(dir_);
+
+ std::wstring history_name(dir_);
+ file_util::AppendToPath(&history_name, L"History");
+ main_db_.reset(new HistoryDatabase);
+ if (main_db_->Init(history_name) != INIT_OK)
+ main_db_.reset();
+
+ std::wstring archived_name(dir_);
+ file_util::AppendToPath(&archived_name, L"Archived History");
+ archived_db_.reset(new ArchivedDatabase);
+ if (!archived_db_->Init(archived_name))
+ archived_db_.reset();
+
+ std::wstring thumb_name(dir_);
+ file_util::AppendToPath(&thumb_name, L"Thumbnails");
+ thumb_db_.reset(new ThumbnailDatabase);
+ if (thumb_db_->Init(thumb_name) != INIT_OK)
+ thumb_db_.reset();
+
+ text_db_.reset(new TextDatabaseManager(dir_, main_db_.get()));
+ if (!text_db_->Init())
+ text_db_.reset();
+
+ expirer_.SetDatabases(main_db_.get(), archived_db_.get(), thumb_db_.get(),
+ text_db_.get());
+ }
+
+ void TearDown() {
+ ClearLastNotifications();
+
+ expirer_.SetDatabases(NULL, NULL, NULL, NULL);
+
+ main_db_.reset();
+ archived_db_.reset();
+ thumb_db_.reset();
+ text_db_.reset();
+ file_util::Delete(dir_, true);
+ }
+
+ // BroadcastNotificationDelegate implementation.
+ void BroadcastNotifications(NotificationType type,
+ HistoryDetails* details_deleted) {
+ // This gets called when there are notifications to broadcast. Instead, we
+ // store them so we can tell that the correct notifications were sent.
+ notifications_.push_back(std::make_pair(type, details_deleted));
+ }
+};
+
+// The example data consists of 4 visits. The middle two visits are to the
+// same URL, while the first and last are for unique ones. This allows a test
+// for the oldest or newest to include both a URL that should get totally
+// deleted (the one on the end) with one that should only get a visit deleted
+// (with the one in the middle) when it picks the proper threshold time.
+//
+// Each visit has indexed data, each URL has thumbnail. The first two URLs will
+// share the same favicon, while the last one will have a unique favicon. The
+// second visit for the middle URL is typed.
+//
+// The IDs of the added URLs, and the times of the four added visits will be
+// added to the given arrays.
+void ExpireHistoryTest::AddExampleData(URLID url_ids[3], Time visit_times[4]) {
+ if (!main_db_.get() || !text_db_.get())
+ return;
+
+ // Four times for each visit.
+ visit_times[3] = Time::Now();
+ visit_times[2] = visit_times[3] - TimeDelta::FromDays(1);
+ visit_times[1] = visit_times[3] - TimeDelta::FromDays(2);
+ visit_times[0] = visit_times[3] - TimeDelta::FromDays(3);
+
+ // Two favicons. The first two URLs will share the same one, while the last
+ // one will have a unique favicon.
+ FavIconID favicon1 = thumb_db_->AddFavIcon(GURL("http://favicon/url1"));
+ FavIconID favicon2 = thumb_db_->AddFavIcon(GURL("http://favicon/url2"));
+
+ // Three URLs.
+ URLRow url_row1(GURL("http://www.google.com/1"));
+ url_row1.set_last_visit(visit_times[0]);
+ url_row1.set_favicon_id(favicon1);
+ url_row1.set_visit_count(1);
+ url_ids[0] = main_db_->AddURL(url_row1);
+
+ URLRow url_row2(GURL("http://www.google.com/2"));
+ url_row2.set_last_visit(visit_times[2]);
+ url_row2.set_favicon_id(favicon1);
+ url_row2.set_visit_count(2);
+ url_row2.set_typed_count(1);
+ url_ids[1] = main_db_->AddURL(url_row2);
+
+ URLRow url_row3(GURL("http://www.google.com/3"));
+ url_row3.set_last_visit(visit_times[3]);
+ url_row3.set_favicon_id(favicon2);
+ url_row3.set_visit_count(1);
+ url_ids[2] = main_db_->AddURL(url_row3);
+
+ // Thumbnails for each URL.
+ scoped_ptr<SkBitmap> thumbnail(
+ JPEGCodec::Decode(kGoogleThumbnail, sizeof(kGoogleThumbnail)));
+ ThumbnailScore score(0.25, true, true, Time::Now());
+ thumb_db_->SetPageThumbnail(url_ids[0], *thumbnail, score);
+ thumb_db_->SetPageThumbnail(url_ids[1], *thumbnail, score);
+ thumb_db_->SetPageThumbnail(url_ids[2], *thumbnail, score);
+
+ // Four visits.
+ VisitRow visit_row1;
+ visit_row1.url_id = url_ids[0];
+ visit_row1.visit_time = visit_times[0];
+ visit_row1.is_indexed = true;
+ main_db_->AddVisit(&visit_row1);
+
+ VisitRow visit_row2;
+ visit_row2.url_id = url_ids[1];
+ visit_row2.visit_time = visit_times[1];
+ visit_row2.is_indexed = true;
+ main_db_->AddVisit(&visit_row2);
+
+ VisitRow visit_row3;
+ visit_row3.url_id = url_ids[1];
+ visit_row3.visit_time = visit_times[2];
+ visit_row3.is_indexed = true;
+ visit_row3.transition = PageTransition::TYPED;
+ main_db_->AddVisit(&visit_row3);
+
+ VisitRow visit_row4;
+ visit_row4.url_id = url_ids[2];
+ visit_row4.visit_time = visit_times[3];
+ visit_row4.is_indexed = true;
+ main_db_->AddVisit(&visit_row4);
+
+ // Full text index for each visit.
+ text_db_->AddPageData(url_row1.url(), visit_row1.url_id, visit_row1.visit_id,
+ visit_row1.visit_time, L"title", L"body");
+
+ text_db_->AddPageData(url_row2.url(), visit_row2.url_id, visit_row2.visit_id,
+ visit_row2.visit_time, L"title", L"body");
+ text_db_->AddPageData(url_row2.url(), visit_row3.url_id, visit_row3.visit_id,
+ visit_row3.visit_time, L"title", L"body");
+
+ // Note the special text in this URL. We'll search the file for this string
+ // to make sure it doesn't hang around after the delete.
+ text_db_->AddPageData(url_row3.url(), visit_row4.url_id, visit_row4.visit_id,
+ visit_row4.visit_time, L"title", L"goats body");
+}
+
+bool ExpireHistoryTest::HasFavIcon(FavIconID favicon_id) {
+ if (!thumb_db_.get())
+ return false;
+ Time last_updated;
+ std::vector<unsigned char> icon_data_unused;
+ GURL icon_url;
+ return thumb_db_->GetFavIcon(favicon_id, &last_updated, &icon_data_unused,
+ &icon_url);
+}
+
+bool ExpireHistoryTest::HasThumbnail(URLID url_id) {
+ std::vector<unsigned char> temp_data;
+ return thumb_db_->GetPageThumbnail(url_id, &temp_data);
+}
+
+int ExpireHistoryTest::CountTextMatchesForURL(const GURL& url) {
+ if (!text_db_.get())
+ return 0;
+
+ // "body" should match all pagesx in the example data.
+ std::vector<TextDatabase::Match> results;
+ QueryOptions options;
+ options.most_recent_visit_only = false;
+ Time first_time;
+ text_db_->GetTextMatches(L"body", options, &results, &first_time);
+
+ int count = 0;
+ for (size_t i = 0; i < results.size(); i++) {
+ if (results[i].url == url)
+ count++;
+ }
+ return count;
+}
+
+void ExpireHistoryTest::EnsureURLInfoGone(const URLRow& row) {
+ // Verify the URL no longer exists.
+ URLRow temp_row;
+ EXPECT_FALSE(main_db_->GetURLRow(row.id(), &temp_row));
+
+ // The indexed data should be gone.
+ EXPECT_EQ(0, CountTextMatchesForURL(row.url()));
+
+ // There should be no visits.
+ VisitVector visits;
+ main_db_->GetVisitsForURL(row.id(), &visits);
+ EXPECT_EQ(0, visits.size());
+
+ // Thumbnail should be gone.
+ EXPECT_FALSE(HasThumbnail(row.id()));
+
+ // Check the notifications. There should be a delete notification with this
+ // URL in it. There should also be a "typed URL changed" notification if the
+ // row is marked typed.
+ bool found_delete_notification = false;
+ bool found_typed_changed_notification = false;
+ for (size_t i = 0; i < notifications_.size(); i++) {
+ if (notifications_[i].first == NOTIFY_HISTORY_URLS_DELETED) {
+ const URLsDeletedDetails* deleted_details =
+ reinterpret_cast<URLsDeletedDetails*>(notifications_[i].second);
+ if (deleted_details->urls.find(row.url()) !=
+ deleted_details->urls.end()) {
+ found_delete_notification = true;
+ }
+ } else if (notifications_[i].first == NOTIFY_HISTORY_TYPED_URLS_MODIFIED) {
+ // See if we got a typed URL changed notification.
+ const URLsModifiedDetails* modified_details =
+ reinterpret_cast<URLsModifiedDetails*>(notifications_[i].second);
+ for (size_t cur_url = 0; cur_url < modified_details->changed_urls.size();
+ cur_url++) {
+ if (modified_details->changed_urls[cur_url].url() == row.url())
+ found_typed_changed_notification = true;
+ }
+ } else if (notifications_[i].first == NOTIFY_HISTORY_URL_VISITED) {
+ // See if we got a visited URL notification.
+ const URLVisitedDetails* visited_details =
+ reinterpret_cast<URLVisitedDetails*>(notifications_[i].second);
+ if (visited_details->row.url() == row.url())
+ found_typed_changed_notification = true;
+ }
+ }
+ EXPECT_TRUE(found_delete_notification);
+ EXPECT_EQ(row.typed_count() > 0, found_typed_changed_notification);
+}
+
+// static
+bool ExpireHistoryTest::IsStringInFile(std::wstring& filename,
+ const char* str) {
+ std::string contents;
+ EXPECT_TRUE(file_util::ReadFileToString(filename, &contents));
+ return contents.find(str) != std::string::npos;
+}
+
+TEST_F(ExpireHistoryTest, DeleteFaviconsIfPossible) {
+ // Add a favicon record.
+ const GURL favicon_url("http://www.google.com/favicon.ico");
+ FavIconID icon_id = thumb_db_->AddFavIcon(favicon_url);
+ EXPECT_TRUE(icon_id);
+ EXPECT_TRUE(HasFavIcon(icon_id));
+
+ // The favicon should be deletable with no users.
+ std::set<FavIconID> favicon_set;
+ favicon_set.insert(icon_id);
+ expirer_.DeleteFaviconsIfPossible(favicon_set);
+ EXPECT_FALSE(HasFavIcon(icon_id));
+
+ // Add back the favicon.
+ icon_id = thumb_db_->AddFavIcon(favicon_url);
+ EXPECT_TRUE(icon_id);
+ EXPECT_TRUE(HasFavIcon(icon_id));
+
+ // Add a page that references the favicon.
+ URLRow row(GURL("http://www.google.com/2"));
+ row.set_visit_count(1);
+ row.set_favicon_id(icon_id);
+ EXPECT_TRUE(main_db_->AddURL(row));
+
+ // Favicon should not be deletable.
+ favicon_set.clear();
+ favicon_set.insert(icon_id);
+ expirer_.DeleteFaviconsIfPossible(favicon_set);
+ EXPECT_TRUE(HasFavIcon(icon_id));
+}
+
+// Deletes a URL with a favicon that it is the last referencer of, so that it
+// should also get deleted.
+TEST_F(ExpireHistoryTest, DeleteURLAndFavicon) {
+ URLID url_ids[3];
+ Time visit_times[4];
+ AddExampleData(url_ids, visit_times);
+
+ // Verify things are the way we expect with a URL row, favicon, thumbnail.
+ URLRow last_row;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &last_row));
+ EXPECT_TRUE(HasFavIcon(last_row.favicon_id()));
+ EXPECT_TRUE(HasThumbnail(url_ids[2]));
+
+ VisitVector visits;
+ main_db_->GetVisitsForURL(url_ids[2], &visits);
+ ASSERT_EQ(1, visits.size());
+ EXPECT_EQ(1, CountTextMatchesForURL(last_row.url()));
+
+ // In this test we also make sure that any pending entries in the text
+ // database manager are removed.
+ text_db_->AddPageURL(last_row.url(), last_row.id(), visits[0].visit_id,
+ visits[0].visit_time);
+
+ // Compute the text DB filename.
+ std::wstring fts_filename = dir_;
+ file_util::AppendToPath(&fts_filename,
+ TextDatabase::IDToFileName(text_db_->TimeToID(visit_times[3])));
+
+ // When checking the file, the database must be closed. We then re-initialize
+ // it just like the test set-up did.
+ text_db_.reset();
+ EXPECT_TRUE(IsStringInFile(fts_filename, "goats"));
+ text_db_.reset(new TextDatabaseManager(dir_, main_db_.get()));
+ ASSERT_TRUE(text_db_->Init());
+ expirer_.SetDatabases(main_db_.get(), archived_db_.get(), thumb_db_.get(),
+ text_db_.get());
+
+ // Delete the URL and its dependencies.
+ expirer_.DeleteURL(last_row.url());
+
+ // The string should be removed from the file. FTS can mark it as gone but
+ // doesn't remove it from the file, we want to be sure we're doing the latter.
+ text_db_.reset();
+ EXPECT_FALSE(IsStringInFile(fts_filename, "goats"));
+ text_db_.reset(new TextDatabaseManager(dir_, main_db_.get()));
+ ASSERT_TRUE(text_db_->Init());
+ expirer_.SetDatabases(main_db_.get(), archived_db_.get(), thumb_db_.get(),
+ text_db_.get());
+
+ // Run the text database expirer. This will flush any pending entries so we
+ // can check that nothing was committed. We use a time far in the future so
+ // that anything added recently will get flushed.
+ TimeTicks expiration_time = TimeTicks::Now() + TimeDelta::FromDays(1);
+ text_db_->FlushOldChangesForTime(expiration_time);
+
+ // All the normal data + the favicon should be gone.
+ EnsureURLInfoGone(last_row);
+ EXPECT_FALSE(HasFavIcon(last_row.favicon_id()));
+}
+
+// Deletes a URL with a favicon that other URLs reference, so that the favicon
+// should not get deleted. This also tests deleting more than one visit.
+TEST_F(ExpireHistoryTest, DeleteURLWithoutFavicon) {
+ URLID url_ids[3];
+ Time visit_times[4];
+ AddExampleData(url_ids, visit_times);
+
+ // Verify things are the way we expect with a URL row, favicon, thumbnail.
+ URLRow last_row;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &last_row));
+ EXPECT_TRUE(HasFavIcon(last_row.favicon_id()));
+ EXPECT_TRUE(HasThumbnail(url_ids[1]));
+
+ VisitVector visits;
+ main_db_->GetVisitsForURL(url_ids[1], &visits);
+ EXPECT_EQ(2, visits.size());
+ EXPECT_EQ(1, CountTextMatchesForURL(last_row.url()));
+
+ // Delete the URL and its dependencies.
+ expirer_.DeleteURL(last_row.url());
+
+ // All the normal data + the favicon should be gone.
+ EnsureURLInfoGone(last_row);
+ EXPECT_TRUE(HasFavIcon(last_row.favicon_id()));
+}
+
+// DeletdeURL should delete URLs that are starred.
+TEST_F(ExpireHistoryTest, DeleteStarredURL) {
+ URLID url_ids[3];
+ Time visit_times[4];
+ AddExampleData(url_ids, visit_times);
+
+ URLRow url_row;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row));
+
+ // Star the last URL.
+ StarredEntry starred;
+ starred.type = StarredEntry::URL;
+ starred.url = url_row.url();
+ starred.url_id = url_row.id();
+ starred.parent_group_id = HistoryService::kBookmarkBarID;
+ ASSERT_TRUE(main_db_->CreateStarredEntry(&starred));
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row));
+
+ // Now delete it, this should delete it even though it's starred.
+ expirer_.DeleteURL(url_row.url());
+
+ // All the normal data + the favicon should be gone.
+ EnsureURLInfoGone(url_row);
+ EXPECT_FALSE(HasFavIcon(url_row.favicon_id()));
+}
+
+// Expires all URLs more recent than a given time, with no starred items.
+// Our time threshold is such that one URL should be updated (we delete one of
+// the two visits) and one is deleted.
+TEST_F(ExpireHistoryTest, FlushRecentURLsUnstarred) {
+ URLID url_ids[3];
+ Time visit_times[4];
+ AddExampleData(url_ids, visit_times);
+
+ URLRow url_row1, url_row2;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row2));
+
+ // In this test we also make sure that any pending entries in the text
+ // database manager are removed.
+ VisitVector visits;
+ main_db_->GetVisitsForURL(url_ids[2], &visits);
+ ASSERT_EQ(1, visits.size());
+ text_db_->AddPageURL(url_row2.url(), url_row2.id(), visits[0].visit_id,
+ visits[0].visit_time);
+
+ // This should delete the last two visits.
+ expirer_.ExpireHistoryBetween(visit_times[2], Time());
+
+ // Run the text database expirer. This will flush any pending entries so we
+ // can check that nothing was committed. We use a time far in the future so
+ // that anything added recently will get flushed.
+ TimeTicks expiration_time = TimeTicks::Now() + TimeDelta::FromDays(1);
+ text_db_->FlushOldChangesForTime(expiration_time);
+
+ // Verify that the middle URL had its last visit deleted only.
+ visits.clear();
+ main_db_->GetVisitsForURL(url_ids[1], &visits);
+ EXPECT_EQ(1, visits.size());
+ EXPECT_EQ(0, CountTextMatchesForURL(url_row1.url()));
+
+ // Verify that the middle URL visit time and visit counts were updated.
+ URLRow temp_row;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &temp_row));
+ EXPECT_TRUE(visit_times[2] == url_row1.last_visit()); // Previous value.
+ EXPECT_TRUE(visit_times[1] == temp_row.last_visit()); // New value.
+ EXPECT_EQ(2, url_row1.visit_count());
+ EXPECT_EQ(1, temp_row.visit_count());
+ EXPECT_EQ(1, url_row1.typed_count());
+ EXPECT_EQ(0, temp_row.typed_count());
+
+ // Verify that the middle URL's favicon and thumbnail is still there.
+ EXPECT_TRUE(HasFavIcon(url_row1.favicon_id()));
+ EXPECT_TRUE(HasThumbnail(url_row1.id()));
+
+ // Verify that the last URL was deleted.
+ EnsureURLInfoGone(url_row2);
+ EXPECT_FALSE(HasFavIcon(url_row2.favicon_id()));
+}
+
+// Expire a starred URL, it shouldn't get deleted and its visit counts should
+// be updated properly.
+TEST_F(ExpireHistoryTest, FlushRecentURLsStarred) {
+ URLID url_ids[3];
+ Time visit_times[4];
+ AddExampleData(url_ids, visit_times);
+
+ URLRow url_row1, url_row2;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row2));
+
+ // Star the last two URLs.
+ StarredEntry starred1;
+ starred1.type = StarredEntry::URL;
+ starred1.url = url_row1.url();
+ starred1.url_id = url_row1.id();
+ starred1.parent_group_id = HistoryService::kBookmarkBarID;
+ ASSERT_TRUE(main_db_->CreateStarredEntry(&starred1));
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
+
+ StarredEntry starred2;
+ starred2.type = StarredEntry::URL;
+ starred2.url = url_row2.url();
+ starred2.url_id = url_row2.id();
+ starred2.parent_group_id = HistoryService::kBookmarkBarID;
+ ASSERT_TRUE(main_db_->CreateStarredEntry(&starred2));
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row2));
+
+ // This should delete the last two visits.
+ expirer_.ExpireHistoryBetween(visit_times[2], Time());
+
+ // The URL rows should still exist.
+ URLRow new_url_row1, new_url_row2;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &new_url_row1));
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &new_url_row2));
+
+ // The visit times should be updated.
+ EXPECT_TRUE(new_url_row1.last_visit() == visit_times[1]);
+ EXPECT_TRUE(new_url_row2.last_visit().is_null()); // No last visit time.
+
+ // Visit counts should be updated.
+ EXPECT_EQ(0, new_url_row1.typed_count());
+ EXPECT_EQ(1, new_url_row1.visit_count());
+ EXPECT_EQ(0, new_url_row2.typed_count());
+ EXPECT_EQ(0, new_url_row2.visit_count());
+
+ // Thumbnails and favicons should still exist. Note that we keep thumbnails
+ // that may have been updated since the time threshold. Since the URL still
+ // exists in history, this should not be a privacy problem, we only update
+ // the visit counts in this case for consistency anyway.
+ EXPECT_TRUE(HasFavIcon(new_url_row1.favicon_id()));
+ EXPECT_TRUE(HasThumbnail(new_url_row1.id()));
+ EXPECT_TRUE(HasFavIcon(new_url_row2.favicon_id()));
+ EXPECT_TRUE(HasThumbnail(new_url_row2.id()));
+}
+
+TEST_F(ExpireHistoryTest, ArchiveHistoryBeforeUnstarred) {
+ URLID url_ids[3];
+ Time visit_times[4];
+ AddExampleData(url_ids, visit_times);
+
+ URLRow url_row1, url_row2;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[2], &url_row2));
+
+ // Archive the oldest two visits. This will actually result in deleting them
+ // since their transition types are empty (not important).
+ expirer_.ArchiveHistoryBefore(visit_times[1]);
+
+ // The first URL should be deleted, the second should not be affected.
+ URLRow temp_row;
+ EXPECT_FALSE(main_db_->GetURLRow(url_ids[0], &temp_row));
+ EXPECT_TRUE(main_db_->GetURLRow(url_ids[1], &temp_row));
+ EXPECT_TRUE(main_db_->GetURLRow(url_ids[2], &temp_row));
+
+ // Make sure the archived database has nothing in it.
+ EXPECT_FALSE(archived_db_->GetRowForURL(url_row1.url(), NULL));
+ EXPECT_FALSE(archived_db_->GetRowForURL(url_row2.url(), NULL));
+
+ // Now archive one more visit so that the middle URL should be removed. This
+ // one will actually be archived instead of deleted.
+ expirer_.ArchiveHistoryBefore(visit_times[2]);
+ EXPECT_FALSE(main_db_->GetURLRow(url_ids[1], &temp_row));
+ EXPECT_TRUE(main_db_->GetURLRow(url_ids[2], &temp_row));
+
+ // Make sure the archived database has an entry for the second URL.
+ URLRow archived_row;
+ // Note that the ID is different in the archived DB, so look up by URL.
+ EXPECT_TRUE(archived_db_->GetRowForURL(url_row1.url(), &archived_row));
+ VisitVector archived_visits;
+ archived_db_->GetVisitsForURL(archived_row.id(), &archived_visits);
+ EXPECT_EQ(1, archived_visits.size());
+}
+
+TEST_F(ExpireHistoryTest, ArchiveHistoryBeforeStarred) {
+ URLID url_ids[3];
+ Time visit_times[4];
+ AddExampleData(url_ids, visit_times);
+
+ URLRow url_row0, url_row1;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[0], &url_row0));
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &url_row1));
+
+ // Star the URLs. We use fake star IDs here, but that doesn't matter.
+ url_row0.set_star_id(1);
+ main_db_->UpdateURLRow(url_row0.id(), url_row0);
+ url_row1.set_star_id(2);
+ main_db_->UpdateURLRow(url_row1.id(), url_row1);
+
+ // Now archive the first three visits (first two URLs). The first two visits
+ // should be, the thirddeleted, but the URL records should not.
+ expirer_.ArchiveHistoryBefore(visit_times[2]);
+
+ // The first URL should have its visit deleted, but it should still be present
+ // in the main DB and not in the archived one since it is starred.
+ URLRow temp_row;
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[0], &temp_row));
+ // Note that the ID is different in the archived DB, so look up by URL.
+ EXPECT_FALSE(archived_db_->GetRowForURL(temp_row.url(), NULL));
+ VisitVector visits;
+ main_db_->GetVisitsForURL(temp_row.id(), &visits);
+ EXPECT_EQ(0, visits.size());
+
+ // The second URL should have its first visit deleted and its second visit
+ // archived. It should be present in both the main DB (because it's starred)
+ // and the archived DB (for the archived visit).
+ ASSERT_TRUE(main_db_->GetURLRow(url_ids[1], &temp_row));
+ main_db_->GetVisitsForURL(temp_row.id(), &visits);
+ EXPECT_EQ(0, visits.size());
+
+ // Note that the ID is different in the archived DB, so look up by URL.
+ ASSERT_TRUE(archived_db_->GetRowForURL(temp_row.url(), &temp_row));
+ archived_db_->GetVisitsForURL(temp_row.id(), &visits);
+ ASSERT_EQ(1, visits.size());
+ EXPECT_TRUE(visit_times[2] == visits[0].visit_time);
+
+ // The third URL should be unchanged.
+ EXPECT_TRUE(main_db_->GetURLRow(url_ids[2], &temp_row));
+ EXPECT_FALSE(archived_db_->GetRowForURL(temp_row.url(), NULL));
+}
+
+// Tests the return values from ArchiveSomeOldHistory. The rest of the
+// functionality of this function is tested by the ArchiveHistoryBefore*
+// tests which use this function internally.
+TEST_F(ExpireHistoryTest, ArchiveSomeOldHistory) {
+ URLID url_ids[3];
+ Time visit_times[4];
+ AddExampleData(url_ids, visit_times);
+
+ // Deleting a time range with no URLs should return false (nothing found).
+ EXPECT_FALSE(expirer_.ArchiveSomeOldHistory(
+ visit_times[0] - TimeDelta::FromDays(100), 1));
+
+ // Deleting a time range with not up the the max results should also return
+ // false (there will only be one visit deleted in this range).
+ EXPECT_FALSE(expirer_.ArchiveSomeOldHistory(visit_times[0], 2));
+
+ // Deleting a time range with the max number of results should return true
+ // (max deleted).
+ EXPECT_TRUE(expirer_.ArchiveSomeOldHistory(visit_times[2], 1));
+}
+
+// TODO(brettw) add some visits with no URL to make sure everything is updated
+// properly. Have the visits also refer to nonexistant FTS rows.
+//
+// Maybe also refer to invalid favicons.
+
+} // namespace history
diff --git a/chrome/browser/history/history.cc b/chrome/browser/history/history.cc
new file mode 100644
index 0000000..d4e6879
--- /dev/null
+++ b/chrome/browser/history/history.cc
@@ -0,0 +1,697 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// The history system runs on a background thread so that potentially slow
+// database operations don't delay the browser. This backend processing is
+// represented by HistoryBackend. The HistoryService's job is to dispatch to
+// that thread.
+//
+// Main thread History thread
+// ----------- --------------
+// HistoryService <----------------> HistoryBackend
+// -> HistoryDatabase
+// -> SQLite connection to History
+// -> ArchivedDatabase
+// -> SQLite connection to Archived History
+// -> TextDatabaseManager
+// -> SQLite connection to one month's data
+// -> SQLite connection to one month's data
+// ...
+// -> ThumbnailDatabase
+// -> SQLite connection to Thumbnails
+// (and favicons)
+
+#include "chrome/browser/history/history.h"
+
+#include "base/file_util.h"
+#include "base/message_loop.h"
+#include "base/path_service.h"
+#include "base/ref_counted.h"
+#include "base/task.h"
+#include "chrome/browser/autocomplete/history_url_provider.h"
+#include "chrome/browser/browser_list.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/history/download_types.h"
+#include "chrome/browser/history/history_backend.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/history/in_memory_database.h"
+#include "chrome/browser/history/in_memory_history_backend.h"
+#include "chrome/browser/visitedlink_master.h"
+#include "chrome/common/chrome_constants.h"
+#include "chrome/common/l10n_util.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/sqlite_utils.h"
+#include "chrome/common/thumbnail_score.h"
+#include "generated_resources.h"
+
+using history::HistoryBackend;
+
+// Sends messages from the backend to us on the main thread. This must be a
+// separate class from the history service so that it can hold a reference to
+// the history service (otherwise we would have to manually AddRef and
+// Release when the Backend has a reference to us).
+class HistoryService::BackendDelegate : public HistoryBackend::Delegate {
+ public:
+ explicit BackendDelegate(HistoryService* history_service)
+ : history_service_(history_service),
+ message_loop_(MessageLoop::current()) {
+ }
+
+ virtual void NotifyTooNew() {
+ // Send the backend to the history service on the main thread.
+ message_loop_->PostTask(FROM_HERE, NewRunnableMethod(history_service_.get(),
+ &HistoryService::NotifyTooNew));
+ }
+
+ virtual void SetInMemoryBackend(
+ history::InMemoryHistoryBackend* backend) {
+ // Send the backend to the history service on the main thread.
+ message_loop_->PostTask(FROM_HERE, NewRunnableMethod(history_service_.get(),
+ &HistoryService::SetInMemoryBackend, backend));
+ }
+
+ virtual void BroadcastNotifications(NotificationType type,
+ history::HistoryDetails* details) {
+ // Send the notification to the history service on the main thread.
+ message_loop_->PostTask(FROM_HERE, NewRunnableMethod(history_service_.get(),
+ &HistoryService::BroadcastNotifications, type, details));
+ }
+
+ private:
+ scoped_refptr<HistoryService> history_service_;
+ MessageLoop* message_loop_;
+};
+
+// static
+const history::StarID HistoryService::kBookmarkBarID = 1;
+
+HistoryService::HistoryService()
+ : thread_(new ChromeThread(ChromeThread::HISTORY)),
+ profile_(NULL) {
+ if (NotificationService::current()) { // Is NULL when running generate_profile.
+ NotificationService::current()->AddObserver(
+ this, NOTIFY_HISTORY_URLS_DELETED, Source<Profile>(profile_));
+ }
+}
+
+HistoryService::HistoryService(Profile* profile)
+ : thread_(new ChromeThread(ChromeThread::HISTORY)),
+ profile_(profile) {
+ NotificationService::current()->AddObserver(
+ this, NOTIFY_HISTORY_URLS_DELETED, Source<Profile>(profile_));
+}
+
+HistoryService::~HistoryService() {
+ // Shutdown the backend. This does nothing if Cleanup was already invoked.
+ Cleanup();
+
+ // Unregister for notifications.
+ if (NotificationService::current()) { // Is NULL when running generate_profile.
+ NotificationService::current()->RemoveObserver(
+ this, NOTIFY_HISTORY_URLS_DELETED, Source<Profile>(profile_));
+ }
+}
+
+bool HistoryService::Init(const std::wstring& history_dir) {
+ if (!thread_->Start())
+ return false;
+
+ // Create the history backend.
+ scoped_refptr<HistoryBackend> backend(
+ new HistoryBackend(history_dir, new BackendDelegate(this)));
+ history_backend_.swap(backend);
+
+ ScheduleAndForget(PRIORITY_UI, &HistoryBackend::Init);
+ return true;
+}
+
+void HistoryService::Cleanup() {
+ if (!thread_) {
+ // We've already cleaned up.
+ return;
+ }
+
+ // Shutdown is a little subtle. The backend's destructor must run on the
+ // history thread since it is not threadsafe. So this thread must not be the
+ // last thread holding a reference to the backend, or a crash could happen.
+ //
+ // We have a reference to the history backend. There is also an extra
+ // reference held by our delegate installed in the backend, which
+ // HistoryBackend::Closing will release. This means if we scheduled a call
+ // to HistoryBackend::Closing and *then* released our backend reference, there
+ // will be a race between us and the backend's Closing function to see who is
+ // the last holder of a reference. If the backend thread's Closing manages to
+ // run before we release our backend refptr, the last reference will be held
+ // by this thread and the destructor will be called from here.
+ //
+ // Therefore, we create a task to run the Closing operation first. This holds
+ // a reference to the backend. Then we release our reference, then we schedule
+ // the task to run. After the task runs, it will delete its reference from
+ // the history thread, ensuring everything works properly.
+ Task* closing_task =
+ NewRunnableMethod(history_backend_.get(), &HistoryBackend::Closing);
+ history_backend_ = NULL;
+ ScheduleTask(PRIORITY_NORMAL, closing_task);
+
+ // Delete the thread, which joins with the background thread. We defensively
+ // NULL the pointer before deleting it in case somebody tries to use it
+ // during shutdown, but this shouldn't happen.
+ ChromeThread* thread = thread_;
+ thread_ = NULL;
+ delete thread;
+}
+
+void HistoryService::NotifyRenderProcessHostDestruction(const void* host) {
+ ScheduleAndForget(PRIORITY_NORMAL,
+ &HistoryBackend::NotifyRenderProcessHostDestruction, host);
+}
+
+history::URLDatabase* HistoryService::in_memory_database() const {
+ if (in_memory_backend_.get())
+ return in_memory_backend_->db();
+ return NULL;
+}
+
+void HistoryService::SetSegmentPresentationIndex(int64 segment_id, int index) {
+ ScheduleAndForget(PRIORITY_UI,
+ &HistoryBackend::SetSegmentPresentationIndex,
+ segment_id, index);
+}
+
+void HistoryService::SetKeywordSearchTermsForURL(const GURL& url,
+ TemplateURL::IDType keyword_id,
+ const std::wstring& term) {
+ ScheduleAndForget(PRIORITY_UI,
+ &HistoryBackend::SetKeywordSearchTermsForURL,
+ url, keyword_id, term);
+}
+
+void HistoryService::DeleteAllSearchTermsForKeyword(
+ TemplateURL::IDType keyword_id) {
+ ScheduleAndForget(PRIORITY_UI,
+ &HistoryBackend::DeleteAllSearchTermsForKeyword,
+ keyword_id);
+}
+
+HistoryService::Handle HistoryService::GetMostRecentKeywordSearchTerms(
+ TemplateURL::IDType keyword_id,
+ const std::wstring& prefix,
+ int max_count,
+ CancelableRequestConsumerBase* consumer,
+ GetMostRecentKeywordSearchTermsCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::GetMostRecentKeywordSearchTerms,
+ consumer,
+ new history::GetMostRecentKeywordSearchTermsRequest(callback),
+ keyword_id, prefix, max_count);
+}
+
+HistoryService::Handle HistoryService::ScheduleDBTask(
+ HistoryDBTask* task,
+ CancelableRequestConsumerBase* consumer) {
+ history::HistoryDBTaskRequest* request = new history::HistoryDBTaskRequest(
+ NewCallback(task, &HistoryDBTask::DoneRunOnMainThread));
+ request->value = task; // The value is the task to execute.
+ return Schedule(PRIORITY_UI, &HistoryBackend::ProcessDBTask, consumer,
+ request);
+}
+
+HistoryService::Handle HistoryService::QuerySegmentUsageSince(
+ CancelableRequestConsumerBase* consumer,
+ const Time from_time,
+ SegmentQueryCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::QuerySegmentUsage,
+ consumer, new history::QuerySegmentUsageRequest(callback),
+ from_time);
+}
+
+void HistoryService::SetOnBackendDestroyTask(Task* task) {
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::SetOnBackendDestroyTask,
+ MessageLoop::current(), task);
+}
+
+void HistoryService::AddPage(const GURL& url,
+ const void* id_scope,
+ int32 page_id,
+ const GURL& referrer,
+ PageTransition::Type transition,
+ const RedirectList& redirects) {
+ AddPage(url, Time::Now(), id_scope, page_id, referrer, transition, redirects);
+}
+
+void HistoryService::AddPage(const GURL& url,
+ Time time,
+ const void* id_scope,
+ int32 page_id,
+ const GURL& referrer,
+ PageTransition::Type transition,
+ const RedirectList& redirects) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+
+ // Filter out unwanted URLs. We don't add auto-subframe URLs. They are a
+ // large part of history (think iframes for ads) and we never display them in
+ // history UI. We will still add manual subframes, which are ones the user
+ // has clicked on to get.
+ if (!CanAddURL(url) || PageTransition::StripQualifier(transition) ==
+ PageTransition::AUTO_SUBFRAME)
+ return;
+
+ // Add link & all redirects to visited link list.
+ VisitedLinkMaster* visited_links;
+ if (profile_ && (visited_links = profile_->GetVisitedLinkMaster())) {
+ visited_links->AddURL(url);
+
+ if (!redirects.empty()) {
+ // We should not be asked to add a page in the middle of a redirect chain.
+ DCHECK(redirects[redirects.size() - 1] == url);
+
+ // We need the !redirects.empty() condition above since size_t is unsigned
+ // and will wrap around when we subtract one from a 0 size.
+ for (size_t i = 0; i < redirects.size() - 1; i++)
+ visited_links->AddURL(redirects[i]);
+ }
+ }
+
+ scoped_refptr<history::HistoryAddPageArgs> request(
+ new history::HistoryAddPageArgs(url, time, id_scope, page_id,
+ referrer, redirects, transition));
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::AddPage, request);
+}
+
+void HistoryService::SetPageTitle(const GURL& url,
+ const std::wstring& title) {
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::SetPageTitle, url, title);
+}
+
+void HistoryService::AddPageWithDetails(const GURL& url,
+ const std::wstring& title,
+ int visit_count,
+ int typed_count,
+ Time last_visit,
+ bool hidden) {
+ // Filter out unwanted URLs.
+ if (!CanAddURL(url))
+ return;
+
+ // Add to the visited links system.
+ VisitedLinkMaster* visited_links;
+ if (profile_ && (visited_links = profile_->GetVisitedLinkMaster()))
+ visited_links->AddURL(url);
+
+ history::URLRow row(url);
+ row.set_title(title);
+ row.set_visit_count(visit_count);
+ row.set_typed_count(typed_count);
+ row.set_last_visit(last_visit);
+ row.set_hidden(hidden);
+
+ std::vector<history::URLRow> rows;
+ rows.push_back(row);
+
+ ScheduleAndForget(PRIORITY_NORMAL,
+ &HistoryBackend::AddPagesWithDetails, rows);
+}
+
+void HistoryService::AddPagesWithDetails(
+ const std::vector<history::URLRow>& info) {
+
+ // Add to the visited links system.
+ VisitedLinkMaster* visited_links;
+ if (profile_ && (visited_links = profile_->GetVisitedLinkMaster())) {
+ std::vector<GURL> urls;
+ urls.reserve(info.size());
+ for (std::vector<history::URLRow>::const_iterator i = info.begin();
+ i != info.end();
+ ++i)
+ urls.push_back(i->url());
+
+ visited_links->AddURLs(urls);
+ }
+
+ ScheduleAndForget(PRIORITY_NORMAL,
+ &HistoryBackend::AddPagesWithDetails, info);
+}
+
+void HistoryService::SetPageContents(const GURL& url,
+ const std::wstring& contents) {
+ if (!CanAddURL(url))
+ return;
+ ScheduleAndForget(PRIORITY_LOW, &HistoryBackend::SetPageContents,
+ url, contents);
+}
+
+void HistoryService::SetPageThumbnail(const GURL& page_url,
+ const SkBitmap& thumbnail,
+ const ThumbnailScore& score) {
+ if (!CanAddURL(page_url))
+ return;
+
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::SetPageThumbnail,
+ page_url, thumbnail, score);
+}
+
+HistoryService::Handle HistoryService::GetPageThumbnail(
+ const GURL& page_url,
+ CancelableRequestConsumerBase* consumer,
+ ThumbnailDataCallback* callback) {
+ return Schedule(PRIORITY_NORMAL, &HistoryBackend::GetPageThumbnail, consumer,
+ new history::GetPageThumbnailRequest(callback), page_url);
+}
+
+HistoryService::Handle HistoryService::GetFavIcon(
+ const GURL& icon_url,
+ CancelableRequestConsumerBase* consumer,
+ FavIconDataCallback* callback) {
+ // We always do image requests at lower-than-UI priority even though they
+ // appear in the UI, since they can take a long time and the user can use the
+ // program without them.
+ return Schedule(PRIORITY_NORMAL, &HistoryBackend::GetFavIcon, consumer,
+ new history::GetFavIconRequest(callback), icon_url);
+}
+
+HistoryService::Handle HistoryService::UpdateFavIconMappingAndFetch(
+ const GURL& page_url,
+ const GURL& icon_url,
+ CancelableRequestConsumerBase* consumer,
+ FavIconDataCallback* callback) {
+ return Schedule(PRIORITY_NORMAL,
+ &HistoryBackend::UpdateFavIconMappingAndFetch, consumer,
+ new history::GetFavIconRequest(callback), page_url, icon_url);
+}
+
+HistoryService::Handle HistoryService::GetFavIconForURL(
+ const GURL& page_url,
+ CancelableRequestConsumerBase* consumer,
+ FavIconDataCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::GetFavIconForURL,
+ consumer, new history::GetFavIconRequest(callback),
+ page_url);
+}
+
+void HistoryService::SetFavIcon(const GURL& page_url,
+ const GURL& icon_url,
+ const std::vector<unsigned char>& image_data) {
+ if (!CanAddURL(page_url))
+ return;
+
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::SetFavIcon,
+ page_url, icon_url,
+ scoped_refptr<RefCountedBytes>(new RefCountedBytes(image_data)));
+}
+
+void HistoryService::SetFavIconOutOfDateForPage(const GURL& page_url) {
+ ScheduleAndForget(PRIORITY_NORMAL,
+ &HistoryBackend::SetFavIconOutOfDateForPage, page_url);
+}
+
+void HistoryService::SetImportedFavicons(
+ const std::vector<history::ImportedFavIconUsage>& favicon_usage) {
+ ScheduleAndForget(PRIORITY_NORMAL,
+ &HistoryBackend::SetImportedFavicons, favicon_usage);
+}
+
+HistoryService::Handle HistoryService::GetAllStarredEntries(
+ CancelableRequestConsumerBase* consumer,
+ GetStarredEntriesCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::GetAllStarredEntries,
+ consumer,
+ new history::GetStarredEntriesRequest(callback));
+}
+
+void HistoryService::UpdateStarredEntry(const history::StarredEntry& entry) {
+ ScheduleAndForget(PRIORITY_UI, &HistoryBackend::UpdateStarredEntry, entry);
+}
+
+HistoryService::Handle HistoryService::CreateStarredEntry(
+ const history::StarredEntry& entry,
+ CancelableRequestConsumerBase* consumer,
+ CreateStarredEntryCallback* callback) {
+ DCHECK(entry.type != history::StarredEntry::BOOKMARK_BAR &&
+ entry.type != history::StarredEntry::OTHER);
+ if (!consumer) {
+ ScheduleTask(PRIORITY_UI,
+ NewRunnableMethod(history_backend_.get(),
+ &HistoryBackend::CreateStarredEntry,
+ scoped_refptr<history::CreateStarredEntryRequest>(),
+ entry));
+ return 0;
+ }
+ return Schedule(PRIORITY_UI, &HistoryBackend::CreateStarredEntry, consumer,
+ new history::CreateStarredEntryRequest(callback), entry);
+}
+
+void HistoryService::DeleteStarredGroup(history::UIStarID group_id) {
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::DeleteStarredGroup,
+ group_id);
+}
+
+void HistoryService::DeleteStarredURL(const GURL& url) {
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::DeleteStarredURL, url);
+}
+
+HistoryService::Handle HistoryService::GetMostRecentStarredEntries(
+ int max_count,
+ CancelableRequestConsumerBase* consumer,
+ GetMostRecentStarredEntriesCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::GetMostRecentStarredEntries,
+ consumer,
+ new history::GetMostRecentStarredEntriesRequest(callback),
+ max_count);
+}
+
+void HistoryService::IterateURLs(URLEnumerator* enumerator) {
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::IterateURLs, enumerator);
+}
+
+HistoryService::Handle HistoryService::QueryURL(
+ const GURL& url,
+ bool want_visits,
+ CancelableRequestConsumerBase* consumer,
+ QueryURLCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::QueryURL, consumer,
+ new history::QueryURLRequest(callback), url, want_visits);
+}
+
+// Downloads -------------------------------------------------------------------
+
+// Handle creation of a download by creating an entry in the history service's
+// 'downloads' table.
+HistoryService::Handle HistoryService::CreateDownload(
+ const DownloadCreateInfo& create_info,
+ CancelableRequestConsumerBase* consumer,
+ HistoryService::DownloadCreateCallback* callback) {
+ return Schedule(PRIORITY_NORMAL, &HistoryBackend::CreateDownload, consumer,
+ new history::DownloadCreateRequest(callback), create_info);
+}
+
+// Handle queries for a list of all downloads in the history database's
+// 'downloads' table.
+HistoryService::Handle HistoryService::QueryDownloads(
+ CancelableRequestConsumerBase* consumer,
+ DownloadQueryCallback* callback) {
+ return Schedule(PRIORITY_NORMAL, &HistoryBackend::QueryDownloads, consumer,
+ new history::DownloadQueryRequest(callback));
+}
+
+// Handle updates for a particular download. This is a 'fire and forget'
+// operation, so we don't need to be called back.
+void HistoryService::UpdateDownload(int64 received_bytes,
+ int32 state,
+ int64 db_handle) {
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::UpdateDownload,
+ received_bytes, state, db_handle);
+}
+
+void HistoryService::RemoveDownload(int64 db_handle) {
+ ScheduleAndForget(PRIORITY_NORMAL,
+ &HistoryBackend::RemoveDownload, db_handle);
+}
+
+void HistoryService::RemoveDownloadsBetween(Time remove_begin,
+ Time remove_end) {
+ ScheduleAndForget(PRIORITY_NORMAL,
+ &HistoryBackend::RemoveDownloadsBetween,
+ remove_begin,
+ remove_end);
+}
+
+HistoryService::Handle HistoryService::SearchDownloads(
+ const std::wstring& search_text,
+ CancelableRequestConsumerBase* consumer,
+ DownloadSearchCallback* callback) {
+ return Schedule(PRIORITY_NORMAL, &HistoryBackend::SearchDownloads, consumer,
+ new history::DownloadSearchRequest(callback), search_text);
+}
+
+HistoryService::Handle HistoryService::QueryHistory(
+ const std::wstring& text_query,
+ const history::QueryOptions& options,
+ CancelableRequestConsumerBase* consumer,
+ QueryHistoryCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::QueryHistory, consumer,
+ new history::QueryHistoryRequest(callback),
+ text_query, options);
+}
+
+HistoryService::Handle HistoryService::QueryRedirectsFrom(
+ const GURL& from_url,
+ CancelableRequestConsumerBase* consumer,
+ QueryRedirectsCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::QueryRedirectsFrom, consumer,
+ new history::QueryRedirectsRequest(callback), from_url);
+}
+
+HistoryService::Handle HistoryService::GetVisitCountToHost(
+ const GURL& url,
+ CancelableRequestConsumerBase* consumer,
+ GetVisitCountToHostCallback* callback) {
+ return Schedule(PRIORITY_UI, &HistoryBackend::GetVisitCountToHost, consumer,
+ new history::GetVisitCountToHostRequest(callback), url);
+}
+
+void HistoryService::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ if (type != NOTIFY_HISTORY_URLS_DELETED) {
+ NOTREACHED();
+ return;
+ }
+
+ // Update the visited link system for deleted URLs. We will update the
+ // visited link system for added URLs as soon as we get the add
+ // notification (we don't have to wait for the backend, which allows us to
+ // be faster to update the state).
+ //
+ // For deleted URLs, we don't typically know what will be deleted since
+ // delete notifications are by time. We would also like to be more
+ // respectful of privacy and never tell the user something is gone when it
+ // isn't. Therefore, we update the delete URLs after the fact.
+ if (!profile_)
+ return; // No profile, probably unit testing.
+ Details<history::URLsDeletedDetails> deleted_details(details);
+ VisitedLinkMaster* visited_links = profile_->GetVisitedLinkMaster();
+ if (!visited_links)
+ return; // Nobody to update.
+ if (deleted_details->all_history)
+ visited_links->DeleteAllURLs();
+ else // Delete individual ones.
+ visited_links->DeleteURLs(deleted_details->urls);
+}
+
+void HistoryService::ScheduleAutocomplete(HistoryURLProvider* provider,
+ HistoryURLProviderParams* params) {
+ ScheduleAndForget(PRIORITY_UI, &HistoryBackend::ScheduleAutocomplete,
+ scoped_refptr<HistoryURLProvider>(provider), params);
+}
+
+void HistoryService::ScheduleTask(SchedulePriority priority,
+ Task* task) {
+ // FIXME(brettw) do prioritization.
+ thread_->message_loop()->PostTask(FROM_HERE, task);
+}
+
+bool HistoryService::CanAddURL(const GURL& url) const {
+ if (!url.is_valid())
+ return false;
+
+ if (url.SchemeIs("javascript") ||
+ url.SchemeIs("chrome-resource") ||
+ url.SchemeIs("view-source"))
+ return false;
+
+ if (url.SchemeIs("about")) {
+ std::string path = url.path();
+ if (path.empty() || LowerCaseEqualsASCII(path, "blank"))
+ return false;
+ // We allow all other about URLs since the user may like to see things
+ // like "about:memory" or "about:histograms" in their history and
+ // autocomplete.
+ }
+
+ return true;
+}
+
+void HistoryService::SetInMemoryBackend(
+ history::InMemoryHistoryBackend* mem_backend) {
+ DCHECK(!in_memory_backend_.get()) << "Setting mem DB twice";
+ in_memory_backend_.reset(mem_backend);
+
+ // The database requires additional initialization once we own it.
+ in_memory_backend_->AttachToHistoryService(profile_);
+}
+
+void HistoryService::NotifyTooNew() {
+ // Find the last browser window to display our message box from.
+ Browser* cur_browser = BrowserList::GetLastActive();
+ HWND cur_hwnd = cur_browser ? cur_browser->GetTopLevelHWND() : NULL;
+
+ std::wstring title = l10n_util::GetString(IDS_PRODUCT_NAME);
+ std::wstring message = l10n_util::GetString(IDS_PROFILE_TOO_NEW_ERROR);
+ MessageBox(cur_hwnd, message.c_str(), title.c_str(),
+ MB_OK | MB_ICONWARNING | MB_TOPMOST);
+}
+
+void HistoryService::DeleteURL(const GURL& url) {
+ // We will update the visited links when we observe the delete notifications.
+ ScheduleAndForget(PRIORITY_NORMAL, &HistoryBackend::DeleteURL, url);
+}
+
+void HistoryService::ExpireHistoryBetween(
+ Time begin_time, Time end_time,
+ CancelableRequestConsumerBase* consumer,
+ ExpireHistoryCallback* callback) {
+
+ // We will update the visited links when we observe the delete notifications.
+ Schedule(PRIORITY_UI, &HistoryBackend::ExpireHistoryBetween, consumer,
+ new history::ExpireHistoryRequest(callback),
+ begin_time, end_time);
+}
+
+void HistoryService::BroadcastNotifications(
+ NotificationType type,
+ history::HistoryDetails* details_deleted) {
+ // We take ownership of the passed-in pointer and delete it. It was made for
+ // us on another thread, so the caller doesn't know when we will handle it.
+ scoped_ptr<history::HistoryDetails> details(details_deleted);
+ // TODO(evanm): this is currently necessitated by generate_profile, which
+ // runs without a browser process. generate_profile should really create
+ // a browser process, at which point this check can then be nuked.
+ if (!g_browser_process)
+ return;
+
+ // The source of all of our notifications is the profile. Note that this
+ // pointer is NULL in unit tests.
+ Source<Profile> source(profile_);
+
+ // The details object just contains the pointer to the object that the
+ // backend has allocated for us. The receiver of the notification will cast
+ // this to the proper type.
+ Details<history::HistoryDetails> det(details_deleted);
+
+ NotificationService::current()->Notify(type, source, det);
+}
diff --git a/chrome/browser/history/history.h b/chrome/browser/history/history.h
new file mode 100644
index 0000000..9dca73a
--- /dev/null
+++ b/chrome/browser/history/history.h
@@ -0,0 +1,839 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_HISTORY_H__
+#define CHROME_BROWSER_HISTORY_HISTORY_H__
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/gfx/rect.h"
+#include "base/lock.h"
+#include "base/ref_counted.h"
+#include "base/scoped_ptr.h"
+#include "base/task.h"
+#include "base/time.h"
+#include "chrome/browser/cancelable_request.h"
+#include "chrome/browser/chrome_thread.h"
+#include "chrome/browser/history/history_notifications.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/template_url.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/page_transition_types.h"
+#include "chrome/common/ref_counted_util.h"
+
+class BookmarkBarModel;
+struct DownloadCreateInfo;
+class GURL;
+class HistoryURLProvider;
+struct HistoryURLProviderParams;
+class InMemoryURLDatabase;
+class MainPagesRequest;
+enum NotificationType;
+class PageUsageData;
+class PageUsageRequest;
+class Profile;
+class SkBitmap;
+struct ThumbnailScore;
+
+namespace history {
+
+class InMemoryHistoryBackend;
+class HistoryBackend;
+class HistoryDatabase;
+class HistoryQueryTest;
+class URLDatabase;
+
+} // namespace history
+
+
+// HistoryDBTask can be used to process arbitrary work on the history backend
+// thread. HistoryDBTask is scheduled using HistoryService::ScheduleDBTask.
+// When HistoryBackend processes the task it invokes RunOnDBThread. Once the
+// task completes and has not been canceled, DoneRunOnMainThread is invoked back
+// on the main thread.
+class HistoryDBTask : public base::RefCountedThreadSafe<HistoryDBTask> {
+ public:
+ virtual ~HistoryDBTask() {}
+
+ // Invoked on the database thread. The return value indicates whether the
+ // task is done. A return value of true signals the task is done and
+ // RunOnDBThread should NOT be invoked again. A return value of false
+ // indicates the task is not done, and should be run again after other
+ // tasks are given a chance to be processed.
+ virtual bool RunOnDBThread(history::HistoryBackend* backend,
+ history::HistoryDatabase* db) = 0;
+
+ // Invoked on the main thread once RunOnDBThread has returned false. This is
+ // only invoked if the request was not canceled and returned true from
+ // RunOnDBThread.
+ virtual void DoneRunOnMainThread() = 0;
+};
+
+// The history service records page titles, and visit times, as well as
+// (eventually) information about autocomplete.
+//
+// This service is thread safe. Each request callback is invoked in the
+// thread that made the request.
+class HistoryService : public CancelableRequestProvider,
+ public NotificationObserver,
+ public base::RefCountedThreadSafe<HistoryService> {
+ public:
+ // Miscellaneous commonly-used types.
+ typedef std::vector<GURL> RedirectList;
+ typedef std::vector<PageUsageData*> PageUsageDataList;
+
+ // ID (both star_id and group_id) of the bookmark bar.
+ // This entry always exists.
+ static const history::StarID kBookmarkBarID;
+
+ // Must call Init after construction.
+ explicit HistoryService(Profile* profile);
+ // The empty constructor is provided only for testing.
+ HistoryService();
+ ~HistoryService();
+
+ // Initializes the history service, returning true on success. On false, do
+ // not call any other functions. The given directory will be used for storing
+ // the history files.
+ bool Init(const std::wstring& history_dir);
+
+ // Called on shutdown, this will tell the history backend to complete and
+ // will release pointers to it. No other functions should be called once
+ // cleanup has happened that may dispatch to the history thread (because it
+ // will be NULL).
+ //
+ // In practice, this will be called by the service manager (BrowserProcess)
+ // when it is being destroyed. Because that reference is being destroyed, it
+ // should be impossible for anybody else to call the service, even if it is
+ // still in memory (pending requests may be holding a reference to us).
+ void Cleanup();
+
+ // RenderProcessHost pointers are used to scope page IDs (see AddPage). These
+ // objects must tell us when they are being destroyed so that we can clear
+ // out any cached data associated with that scope.
+ //
+ // The given pointer will not be dereferenced, it is only used for
+ // identification purposes, hence it is a void*.
+ void NotifyRenderProcessHostDestruction(const void* host);
+
+ // Returns the in-memory URL database. The returned pointer MAY BE NULL if
+ // the in-memory database has not been loaded yet. This pointer is owned
+ // by the history system. Callers should not store or cache this value.
+ //
+ // TODO(brettw) this should return the InMemoryHistoryBackend.
+ history::URLDatabase* in_memory_database() const;
+
+ // Navigation ----------------------------------------------------------------
+
+ // Adds the given canonical URL to history with the current time as the visit
+ // time. Referrer may be the empty string.
+ //
+ // The supplied render process host is used to scope the given page ID. Page
+ // IDs are only unique inside a given render process, so we need that to
+ // differentiate them. This pointer should not be dereferenced by the history
+ // system. Since render view host pointers may be reused (if one gets deleted
+ // and a new one created at the same address), WebContents should notify
+ // us when they are being destroyed through NotifyWebContentsDestruction.
+ //
+ // The scope/ids can be NULL if there is no meaningful tracking information
+ // that can be performed on the given URL. The 'page_id' should be the ID of
+ // the current session history entry in the given process.
+ //
+ // 'redirects' is an array of redirect URLs leading to this page, with the
+ // page itself as the last item (so when there is no redirect, it will have
+ // one entry). If there are no redirects, this array may also be empty for
+ // the convenience of callers.
+ //
+ // All "Add Page" functions will update the visited link database.
+ void AddPage(const GURL& url,
+ const void* id_scope,
+ int32 page_id,
+ const GURL& referrer,
+ PageTransition::Type transition,
+ const RedirectList& redirects);
+
+ // For adding pages to history with a specific time. This is for testing
+ // purposes. Call the previous one to use the current time.
+ void AddPage(const GURL& url,
+ Time time,
+ const void* id_scope,
+ int32 page_id,
+ const GURL& referrer,
+ PageTransition::Type transition,
+ const RedirectList& redirects);
+
+ // For adding pages to history where no tracking information can be done.
+ void AddPage(const GURL& url) {
+ AddPage(url, NULL, 0, GURL::EmptyGURL(), PageTransition::LINK,
+ RedirectList());
+ }
+
+ // Sets the title for the given page. The page should be in history. If it
+ // is not, this operation is ignored. This call will not update the full
+ // text index. The last title set when the page is indexed will be the
+ // title in the full text index.
+ void SetPageTitle(const GURL& url, const std::wstring& title);
+
+ // Indexing ------------------------------------------------------------------
+
+ // Notifies history of the body text of the given recently-visited URL.
+ // If the URL was not visited "recently enough," the history system may
+ // discard it.
+ void SetPageContents(const GURL& url, const std::wstring& contents);
+
+ // Querying ------------------------------------------------------------------
+
+ // Callback class that a client can implement to iterate over URLs. The
+ // callbacks WILL BE CALLED ON THE BACKGROUND THREAD! Your implementation
+ // should handle this appropriately.
+ class URLEnumerator {
+ public:
+ virtual ~URLEnumerator() {}
+
+ // Indicates that a URL is available. There will be exactly one call for
+ // every URL in history.
+ virtual void OnURL(const GURL& url) = 0;
+
+ // Indicates we are done iterating over URLs. Once called, there will be no
+ // more callbacks made. This call is guaranteed to occur, even if there are
+ // no URLs. If all URLs were iterated, success will be true.
+ virtual void OnComplete(bool success) = 0;
+ };
+
+ // Enumerate all URLs in history. The given iterator will be owned by the
+ // caller, so the caller should ensure it exists until OnComplete is called.
+ // You should not generally use this since it will be slow to slurp all URLs
+ // in from the database. It is designed for rebuilding the visited link
+ // database from history.
+ void IterateURLs(URLEnumerator* iterator);
+
+ // Returns the information about the requested URL. If the URL is found,
+ // success will be true and the information will be in the URLRow parameter.
+ // On success, the visits, if requested, will be sorted by date. If they have
+ // not been requested, the pointer will be valid, but the vector will be
+ // empty.
+ //
+ // If success is false, neither the row nor the vector will be valid.
+ typedef Callback4<Handle,
+ bool, // Success flag, when false, nothing else is valid.
+ const history::URLRow*,
+ history::VisitVector*>::Type
+ QueryURLCallback;
+
+ // Queries the basic information about the URL in the history database. If
+ // the caller is interested in the visits (each time the URL is visited),
+ // set |want_visits| to true. If these are not needed, the function will be
+ // faster by setting this to false.
+ Handle QueryURL(const GURL& url,
+ bool want_visits,
+ CancelableRequestConsumerBase* consumer,
+ QueryURLCallback* callback);
+
+ // Provides the result of a query. See QueryResults in history_types.h.
+ // The common use will be to use QueryResults.Swap to suck the contents of
+ // the results out of the passed in parameter and take ownership of them.
+ typedef Callback2<Handle, history::QueryResults*>::Type
+ QueryHistoryCallback;
+
+ // Queries all history with the given options (see QueryOptions in
+ // history_types.h). If non-empty, the full-text database will be queried with
+ // the given |text_query|. If empty, all results matching the given options
+ // will be returned.
+ //
+ // This isn't totally hooked up yet, this will query the "new" full text
+ // database (see SetPageContents) which won't generally be set yet.
+ Handle QueryHistory(const std::wstring& text_query,
+ const history::QueryOptions& options,
+ CancelableRequestConsumerBase* consumer,
+ QueryHistoryCallback* callback);
+
+ // Called when the results of QueryRedirectsFrom are available.
+ // The given vector will contain a list of all redirects, not counting
+ // the original page. If A redirects to B, the vector will contain only B,
+ // and A will be in 'source_url'.
+ //
+ // If there is no such URL in the database or the most recent visit has no
+ // redirect, the vector will be empty. If the history system failed for
+ // some reason, success will additionally be false. If the given page
+ // has redirected to multiple destinations, this will pick a random one.
+ typedef Callback4<Handle,
+ GURL, // from_url
+ bool, // success
+ RedirectList*>::Type
+ QueryRedirectsCallback;
+
+ // Schedules a query for the most recent redirect coming out of the given
+ // URL. See the RedirectQuerySource above, which is guaranteed to be called
+ // if the request is not canceled.
+ Handle QueryRedirectsFrom(const GURL& from_url,
+ CancelableRequestConsumerBase* consumer,
+ QueryRedirectsCallback* callback);
+
+ typedef Callback4<Handle,
+ bool, // Were we able to determine the # of visits?
+ int, // Number of visits.
+ Time>::Type // Time of first visit. Only first bool is
+ // true and int is > 0.
+ GetVisitCountToHostCallback;
+
+ // Requests the number of visits to all urls on the scheme/host/post
+ // identified by url. This is only valid for http and https urls.
+ Handle GetVisitCountToHost(const GURL& url,
+ CancelableRequestConsumerBase* consumer,
+ GetVisitCountToHostCallback* callback);
+
+ // Thumbnails ----------------------------------------------------------------
+
+ // Implemented by consumers to get thumbnail data. Called when a request for
+ // the thumbnail data is complete. Once this callback is made, the request
+ // will be completed and no other calls will be made for that handle.
+ //
+ // This function will be called even on error conditions or if there is no
+ // thumbnail for that page. In these cases, the data pointer will be NULL.
+ typedef Callback2<Handle, scoped_refptr<RefCountedBytes> >::Type
+ ThumbnailDataCallback;
+
+ // Sets the thumbnail for a given URL. The URL must be in the history
+ // database or the request will be ignored.
+ void SetPageThumbnail(const GURL& url,
+ const SkBitmap& thumbnail,
+ const ThumbnailScore& score);
+
+ // Requests a page thumbnail. See ThumbnailDataCallback definition above.
+ Handle GetPageThumbnail(const GURL& page_url,
+ CancelableRequestConsumerBase* consumer,
+ ThumbnailDataCallback* callback);
+
+ // Favicon -------------------------------------------------------------------
+
+ // Callback for GetFavIcon. If we have previously inquired about the favicon
+ // for this URL, |know_favicon| will be true, and the rest of the fields will
+ // be valid (otherwise they will be ignored).
+ //
+ // On |know_favicon| == true, |data| will either contain the PNG encoded
+ // favicon data, or it will be NULL to indicate that the site does not have
+ // a favicon (in other words, we know the site doesn't have a favicon, as
+ // opposed to not knowing anything). |expired| will be set to true if we
+ // refreshed the favicon "too long" ago and should be updated if the page
+ // is visited again.
+ typedef Callback5<Handle, // handle
+ bool, // know_favicon
+ scoped_refptr<RefCountedBytes>, // data
+ bool, // expired
+ GURL>::Type // url of the favicon
+ FavIconDataCallback;
+
+ // Requests the favicon. FavIconConsumer is notified
+ // when the bits have been fetched. The consumer is NOT deleted by the
+ // HistoryService, and must be valid until the request is serviced.
+ Handle GetFavIcon(const GURL& icon_url,
+ CancelableRequestConsumerBase* consumer,
+ FavIconDataCallback* callback);
+
+ // Fetches the favicon at icon_url, sending the results to the given callback.
+ // If the favicon has previously been set via SetFavIcon(), then the favicon
+ // url for page_url and all redirects is set to icon_url. If the favicon has
+ // not been set, the database is not updated.
+ Handle UpdateFavIconMappingAndFetch(const GURL& page_url,
+ const GURL& icon_url,
+ CancelableRequestConsumerBase* consumer,
+ FavIconDataCallback* callback);
+
+ // Requests a favicon for a web page URL. FavIconConsumer is notified
+ // when the bits have been fetched. The consumer is NOT deleted by the
+ // HistoryService, and must be valid until the request is serviced.
+ //
+ // Note: this version is intended to be used to retrieve the favicon of a
+ // page that has been browsed in the past. |expired| in the callback is
+ // always false.
+ Handle GetFavIconForURL(const GURL& page_url,
+ CancelableRequestConsumerBase* consumer,
+ FavIconDataCallback* callback);
+
+ // Sets the favicon for a page.
+ void SetFavIcon(const GURL& page_url,
+ const GURL& icon_url,
+ const std::vector<unsigned char>& image_data);
+
+ // Marks the favicon for the page as being out of date.
+ void SetFavIconOutOfDateForPage(const GURL& page_url);
+
+ // Allows the importer to set many favicons for many pages at once. The pages
+ // must exist, any favicon sets for unknown pages will be discarded. Existing
+ // favicons will not be overwritten.
+ void SetImportedFavicons(
+ const std::vector<history::ImportedFavIconUsage>& favicon_usage);
+
+ // Starring ------------------------------------------------------------------
+
+ // Starring mutation methods are private, go through the BookmarkBarModel
+ // instead.
+ //
+ // The typedefs are public to allow template magic to work.
+
+ typedef Callback2<Handle, std::vector<history::StarredEntry>* >::Type
+ GetStarredEntriesCallback;
+
+ typedef Callback2<Handle, history::StarID>::Type CreateStarredEntryCallback;
+
+ typedef Callback2<Handle, std::vector<history::StarredEntry>* >::Type
+ GetMostRecentStarredEntriesCallback;
+
+ // Fetches up to max_count starred entries of type URL.
+ // The results are ordered by date added in descending order (most recent
+ // first).
+ Handle GetMostRecentStarredEntries(
+ int max_count,
+ CancelableRequestConsumerBase* consumer,
+ GetMostRecentStarredEntriesCallback* callback);
+
+ // Database management operations --------------------------------------------
+
+ // Delete all the information related to a single url.
+ void DeleteURL(const GURL& url);
+
+ // Implemented by the caller of 'ExpireHistory(Since|Between)' below, and
+ // is called when the history service has deleted the history.
+ typedef Callback0::Type ExpireHistoryCallback;
+
+ // Removes all visits in the selected time range (including the start time),
+ // updating the URLs accordingly. This deletes the associated data, including
+ // the full text index. This function also deletes the associated favicons,
+ // if they are no longer referenced. |callback| runs when the expiration is
+ // complete. You may use null Time values to do an unbounded delete in
+ // either direction.
+ void ExpireHistoryBetween(Time begin_time, Time end_time,
+ CancelableRequestConsumerBase* consumer,
+ ExpireHistoryCallback* callback);
+
+ // Downloads -----------------------------------------------------------------
+
+ // Implemented by the caller of 'CreateDownload' below, and is called when the
+ // history service has created a new entry for a download in the history db.
+ typedef Callback2<DownloadCreateInfo, int64>::Type DownloadCreateCallback;
+
+ // Begins a history request to create a new persistent entry for a download.
+ // 'info' contains all the download's creation state, and 'callback' runs
+ // when the history service request is complete.
+ Handle CreateDownload(const DownloadCreateInfo& info,
+ CancelableRequestConsumerBase* consumer,
+ DownloadCreateCallback* callback);
+
+ // Implemented by the caller of 'QueryDownloads' below, and is called when the
+ // history service has retrieved a list of all download state. The call
+ typedef Callback1<std::vector<DownloadCreateInfo>*>::Type
+ DownloadQueryCallback;
+
+ // Begins a history request to retrieve the state of all downloads in the
+ // history db. 'callback' runs when the history service request is complete,
+ // at which point 'info' contains an array of DownloadCreateInfo, one per
+ // download.
+ Handle QueryDownloads(CancelableRequestConsumerBase* consumer,
+ DownloadQueryCallback* callback);
+
+ // Called to update the history service about the current state of a download.
+ // This is a 'fire and forget' query, so just pass the relevant state info to
+ // the database with no need for a callback.
+ void UpdateDownload(int64 received_bytes, int32 state, int64 db_handle);
+
+ // Permanently remove a download from the history system. This is a 'fire and
+ // forget' operation.
+ void RemoveDownload(int64 db_handle);
+
+ // Permanently removes all completed download from the history system within
+ // the specified range. This function does not delete downloads that are in
+ // progress or in the process of being cancelled. This is a 'fire and forget'
+ // operation. You can pass is_null times to get unbounded time in either or
+ // both directions.
+ void RemoveDownloadsBetween(Time remove_begin, Time remove_end);
+
+ // Implemented by the caller of 'SearchDownloads' below, and is called when
+ // the history system has retrieved the search results.
+ typedef Callback2<Handle, std::vector<int64>*>::Type DownloadSearchCallback;
+
+ // Search for downloads that match the search text.
+ Handle SearchDownloads(const std::wstring& search_text,
+ CancelableRequestConsumerBase* consumer,
+ DownloadSearchCallback* callback);
+
+ // Visit Segments ------------------------------------------------------------
+
+ typedef Callback2<Handle, std::vector<PageUsageData*>*>::Type
+ SegmentQueryCallback;
+
+ // Query usage data for all visit segments since the provided time.
+ //
+ // The request is performed asynchronously and can be cancelled by using the
+ // returned handle.
+ //
+ // The vector provided to the callback and its contents is owned by the
+ // history system. It will be deeply deleted after the callback is invoked.
+ // If you want to preserve any PageUsageData instance, simply remove them
+ // from the vector.
+ //
+ // The vector contains a list of PageUsageData. Each PageUsageData ID is set
+ // to the segment ID. The URL and all the other information is set to the page
+ // representing the segment.
+ Handle QuerySegmentUsageSince(CancelableRequestConsumerBase* consumer,
+ const Time from_time,
+ SegmentQueryCallback* callback);
+
+ // Set the presentation index for the segment identified by |segment_id|.
+ void SetSegmentPresentationIndex(int64 segment_id, int index);
+
+ // Keyword search terms -----------------------------------------------------
+
+ // Sets the search terms for the specified url and keyword. url_id gives the
+ // id of the url, keyword_id the id of the keyword and term the search term.
+ void SetKeywordSearchTermsForURL(const GURL& url,
+ TemplateURL::IDType keyword_id,
+ const std::wstring& term);
+
+ // Deletes all search terms for the specified keyword.
+ void DeleteAllSearchTermsForKeyword(TemplateURL::IDType keyword_id);
+
+ typedef Callback2<Handle, std::vector<history::KeywordSearchTermVisit>*>::Type
+ GetMostRecentKeywordSearchTermsCallback;
+
+ // Returns up to max_count of the most recent search terms starting with the
+ // specified text. The matching is case insensitive. The results are ordered
+ // in descending order up to |max_count| with the most recent search term
+ // first.
+ Handle GetMostRecentKeywordSearchTerms(
+ TemplateURL::IDType keyword_id,
+ const std::wstring& prefix,
+ int max_count,
+ CancelableRequestConsumerBase* consumer,
+ GetMostRecentKeywordSearchTermsCallback* callback);
+
+ // Generic Stuff -------------------------------------------------------------
+
+ typedef Callback0::Type HistoryDBTaskCallback;
+
+ // Schedules a HistoryDBTask for running on the history backend thread. See
+ // HistoryDBTask for details on what this does.
+ Handle ScheduleDBTask(HistoryDBTask* task,
+ CancelableRequestConsumerBase* consumer);
+
+ // Testing -------------------------------------------------------------------
+
+ // Designed for unit tests, this passes the given task on to the history
+ // backend to be called once the history backend has terminated. This allows
+ // callers to know when the history thread is complete and the database files
+ // can be deleted and the next test run. Otherwise, the history thread may
+ // still be running, causing problems in subsequent tests.
+ //
+ // There can be only one closing task, so this will override any previously
+ // set task. We will take ownership of the pointer and delete it when done.
+ // The task will be run on the calling thread (this function is threadsafe).
+ void SetOnBackendDestroyTask(Task* task);
+
+ // Used for unit testing and potentially importing to get known information
+ // into the database. This assumes the URL doesn't exist in the database
+ //
+ // Calling this function many times may be slow because each call will
+ // dispatch to the history thread and will be a separate database
+ // transaction. If this functionality is needed for importing many URLs, a
+ // version that takes an array should probably be added.
+ void AddPageWithDetails(const GURL& url,
+ const std::wstring& title,
+ int visit_count,
+ int typed_count,
+ Time last_visit,
+ bool hidden);
+
+ // The same as AddPageWithDetails() but takes a vector.
+ void AddPagesWithDetails(const std::vector<history::URLRow>& info);
+
+ private:
+ class BackendDelegate;
+ friend class BackendDelegate;
+
+ // These are not currently used, hopefully we can do something in the future
+ // to ensure that the most important things happen first.
+ enum SchedulePriority {
+ PRIORITY_UI, // The highest priority (must respond to UI events).
+ PRIORITY_NORMAL, // Normal stuff like adding a page.
+ PRIORITY_LOW, // Low priority things like indexing or expiration.
+ };
+
+ friend class BookmarkBarModel;
+ friend class HistoryURLProvider;
+ friend class history::HistoryBackend;
+ template<typename Info, typename Callback> friend class DownloadRequest;
+ friend class PageUsageRequest;
+ friend class RedirectRequest;
+ friend class FavIconRequest;
+ friend class history::HistoryQueryTest;
+ friend class HistoryOperation;
+ friend class HistoryURLProviderTest;
+
+ // Starring ------------------------------------------------------------------
+
+ // These are private as they should only be invoked from the bookmark bar
+ // model.
+
+ // Fetches all the starred entries (both groups and entries).
+ Handle GetAllStarredEntries(
+ CancelableRequestConsumerBase* consumer,
+ GetStarredEntriesCallback* callback);
+
+ // Updates the title, parent and visual order of the specified entry. The key
+ // used to identify the entry is NOT entry.id, rather it is the url (if the
+ // type is URL), or the group_id (if the type is other than URL).
+ //
+ // This can NOT be used to change the type of an entry.
+ //
+ // After updating the entry, NOTIFY_STAR_ENTRY_CHANGED is sent.
+ void UpdateStarredEntry(const history::StarredEntry& entry);
+
+ // Creates a starred entry at the specified position. This can be used
+ // for creating groups and nodes.
+ //
+ // If the entry is a URL and the URL is already starred, this behaves the
+ // same as invoking UpdateStarredEntry. If the entry is a URL and the URL is
+ // not starred, the URL is starred appropriately.
+ //
+ // This honors the title, parent_group_id, visual_order and url (for URL
+ // nodes) of the specified entry. All other attributes are ignored.
+ //
+ // NOTE: consumer and callback may be null, in which case the request
+ // isn't cancelable and 0 is returned.
+ Handle CreateStarredEntry(const history::StarredEntry& entry,
+ CancelableRequestConsumerBase* consumer,
+ CreateStarredEntryCallback* callback);
+
+ // Deletes the specified starred group. All children groups are deleted and
+ // starred descendants unstarred. If successful, this sends out the
+ // notification NOTIFY_URLS_STARRED. To delete a starred URL, do
+ // DeletedStarredEntry(id).
+ void DeleteStarredGroup(history::UIStarID group_id);
+
+ // Deletes the specified starred URL. If successful, this sends out the
+ // notification NOTIFY_URLS_STARRED.
+ void DeleteStarredURL(const GURL& url);
+
+ // Implementation of NotificationObserver.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ // Called by the HistoryURLProvider class to schedule an autocomplete, it
+ // will be called back on the internal history thread with the history
+ // database so it can query. See history_autocomplete.cc for a diagram.
+ void ScheduleAutocomplete(HistoryURLProvider* provider,
+ HistoryURLProviderParams* params);
+
+ // Broadcasts the given notification. This is called by the backend so that
+ // the notification will be broadcast on the main thread.
+ //
+ // The |details_deleted| pointer will be sent as the "details" for the
+ // notification. The function takes ownership of the pointer and deletes it
+ // when the notification is sent (it is coming from another thread, so must
+ // be allocated on the heap).
+ void BroadcastNotifications(NotificationType type,
+ history::HistoryDetails* details_deleted);
+
+ // Returns true if this looks like the type of URL we want to add to the
+ // history. We filter out some URLs such as JavaScript.
+ bool CanAddURL(const GURL& url) const;
+
+ // Sets the in-memory URL database. This is called by the backend once the
+ // database is loaded to make it available.
+ void SetInMemoryBackend(history::InMemoryHistoryBackend* mem_backend);
+
+ // Called by our BackendDelegate when the database version is too new to be
+ // read properly.
+ void NotifyTooNew();
+
+ // Call to schedule a given task for running on the history thread with the
+ // specified priority. The task will have ownership taken.
+ void ScheduleTask(SchedulePriority priority, Task* task);
+
+ // Schedule ------------------------------------------------------------------
+ //
+ // Functions for scheduling operations on the history thread that have a
+ // handle and are cancelable. For fire-and-forget operations, see
+ // ScheduleAndForget below.
+
+ template<typename BackendFunc, class RequestType>
+ Handle Schedule(SchedulePriority priority,
+ BackendFunc func, // Function to call on the HistoryBackend.
+ CancelableRequestConsumerBase* consumer,
+ RequestType* request) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ AddRequest(request, consumer);
+ ScheduleTask(priority,
+ NewRunnableMethod(history_backend_.get(), func,
+ scoped_refptr<RequestType>(request)));
+ return request->handle();
+ }
+
+ template<typename BackendFunc, class RequestType, typename ArgA>
+ Handle Schedule(SchedulePriority priority,
+ BackendFunc func, // Function to call on the HistoryBackend.
+ CancelableRequestConsumerBase* consumer,
+ RequestType* request,
+ const ArgA& a) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ AddRequest(request, consumer);
+ ScheduleTask(priority,
+ NewRunnableMethod(history_backend_.get(), func,
+ scoped_refptr<RequestType>(request),
+ a));
+ return request->handle();
+ }
+
+ template<typename BackendFunc,
+ class RequestType, // Descendant of CancelableRequstBase.
+ typename ArgA,
+ typename ArgB>
+ Handle Schedule(SchedulePriority priority,
+ BackendFunc func, // Function to call on the HistoryBackend.
+ CancelableRequestConsumerBase* consumer,
+ RequestType* request,
+ const ArgA& a,
+ const ArgB& b) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ AddRequest(request, consumer);
+ ScheduleTask(priority,
+ NewRunnableMethod(history_backend_.get(), func,
+ scoped_refptr<RequestType>(request),
+ a, b));
+ return request->handle();
+ }
+
+ template<typename BackendFunc,
+ class RequestType, // Descendant of CancelableRequstBase.
+ typename ArgA,
+ typename ArgB,
+ typename ArgC>
+ Handle Schedule(SchedulePriority priority,
+ BackendFunc func, // Function to call on the HistoryBackend.
+ CancelableRequestConsumerBase* consumer,
+ RequestType* request,
+ const ArgA& a,
+ const ArgB& b,
+ const ArgC& c) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ AddRequest(request, consumer);
+ ScheduleTask(priority,
+ NewRunnableMethod(history_backend_.get(), func,
+ scoped_refptr<RequestType>(request),
+ a, b, c));
+ return request->handle();
+ }
+
+ // ScheduleAndForget ---------------------------------------------------------
+ //
+ // Functions for scheduling operations on the history thread that do not need
+ // any callbacks and are not cancelable.
+
+ template<typename BackendFunc>
+ void ScheduleAndForget(SchedulePriority priority,
+ BackendFunc func) { // Function to call on backend.
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ ScheduleTask(priority, NewRunnableMethod(history_backend_.get(), func));
+ }
+
+ template<typename BackendFunc, typename ArgA>
+ void ScheduleAndForget(SchedulePriority priority,
+ BackendFunc func, // Function to call on backend.
+ const ArgA& a) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ ScheduleTask(priority, NewRunnableMethod(history_backend_.get(), func, a));
+ }
+
+ template<typename BackendFunc, typename ArgA, typename ArgB>
+ void ScheduleAndForget(SchedulePriority priority,
+ BackendFunc func, // Function to call on backend.
+ const ArgA& a,
+ const ArgB& b) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ ScheduleTask(priority, NewRunnableMethod(history_backend_.get(), func,
+ a, b));
+ }
+
+ template<typename BackendFunc, typename ArgA, typename ArgB, typename ArgC>
+ void ScheduleAndForget(SchedulePriority priority,
+ BackendFunc func, // Function to call on backend.
+ const ArgA& a,
+ const ArgB& b,
+ const ArgC& c) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ ScheduleTask(priority, NewRunnableMethod(history_backend_.get(), func,
+ a, b, c));
+ }
+
+ template<typename BackendFunc,
+ typename ArgA,
+ typename ArgB,
+ typename ArgC,
+ typename ArgD>
+ void ScheduleAndForget(SchedulePriority priority,
+ BackendFunc func, // Function to call on backend.
+ const ArgA& a,
+ const ArgB& b,
+ const ArgC& c,
+ const ArgD& d) {
+ DCHECK(history_backend_) << "History service being called after cleanup";
+ ScheduleTask(priority, NewRunnableMethod(history_backend_.get(), func,
+ a, b, c, d));
+ }
+
+ // Some void primitives require some internal processing in the main thread
+ // when done. We use this internal consumer for this purpose.
+ CancelableRequestConsumer internal_consumer_;
+
+ // The thread used by the history service to run complicated operations
+ ChromeThread* thread_;
+
+ // This class has most of the implementation and runs on the 'thread_'.
+ // You MUST communicate with this class ONLY through the thread_'s
+ // message_loop().
+ //
+ // This pointer will be NULL once Cleanup() has been called, meaning no
+ // more calls should be made to the history thread.
+ scoped_refptr<history::HistoryBackend> history_backend_;
+
+ // A cache of the user-typed URLs kept in memory that is used by the
+ // autocomplete system. This will be NULL until the database has been created
+ // on the background thread.
+ scoped_ptr<history::InMemoryHistoryBackend> in_memory_backend_;
+
+ // The profile, may be null when testing.
+ Profile* profile_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(HistoryService);
+};
+
+#endif // CHROME_BROWSER_HISTORY_HISTORY_H__
diff --git a/chrome/browser/history/history_backend.cc b/chrome/browser/history/history_backend.cc
new file mode 100644
index 0000000..d9ef224
--- /dev/null
+++ b/chrome/browser/history/history_backend.cc
@@ -0,0 +1,2060 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/history_backend.h"
+
+#include <set>
+
+#include "base/file_util.h"
+#include "base/histogram.h"
+#include "base/message_loop.h"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "base/time.h"
+#include "chrome/browser/autocomplete/history_url_provider.h"
+#include "chrome/browser/history/download_types.h"
+#include "chrome/browser/history/in_memory_history_backend.h"
+#include "chrome/browser/history/page_usage_data.h"
+#include "chrome/common/chrome_constants.h"
+#include "chrome/common/notification_types.h"
+#include "chrome/common/sqlite_utils.h"
+#include "googleurl/src/gurl.h"
+#include "net/base/registry_controlled_domain.h"
+
+/* The HistoryBackend consists of a number of components:
+
+ HistoryDatabase (stores past 3 months of history)
+ StarredURLDatabase (stores starred pages)
+ URLDatabase (stores a list of URLs)
+ DownloadDatabase (stores a list of downloads)
+ VisitDatabase (stores a list of visits for the URLs)
+ VisitSegmentDatabase (stores groups of URLs for the most visited view).
+
+ ArchivedDatabase (stores history older than 3 months)
+ URLDatabase (stores a list of URLs)
+ DownloadDatabase (stores a list of downloads)
+ VisitDatabase (stores a list of visits for the URLs)
+
+ (this does not store starred things or visit segments, all starred info
+ is stored in HistoryDatabase, and visit segments expire after 3 mos.)
+
+ TextDatabaseManager (manages multiple text database for different times)
+ TextDatabase (represents a single month of full-text index).
+ ...more TextDatabase objects...
+
+ ExpireHistoryBackend (manages moving things from HistoryDatabase to
+ the ArchivedDatabase and deleting)
+*/
+
+namespace history {
+
+// How long we keep segment data for in days. Currently 3 months.
+// This value needs to be greater or equal to
+// MostVisitedModel::kMostVisitedScope but we don't want to introduce a direct
+// dependency between MostVisitedModel and the history backend.
+static const int kSegmentDataRetention = 90;
+
+// The number of milliseconds we'll wait to do a commit, so that things are
+// batched together.
+static const int kCommitIntervalMs = 10000;
+
+// The amount of time before we re-fetch the favicon.
+static const int kFavIconRefetchDays = 7;
+
+// GetSessionTabs returns all open tabs, or tabs closed kSessionCloseTimeWindow
+// seconds ago.
+static const int kSessionCloseTimeWindowSecs = 10;
+
+// The maximum number of items we'll allow in the redirect list before
+// deleting some.
+static const int kMaxRedirectCount = 32;
+
+// The number of days old a history entry can be before it is considered "old"
+// and is archived.
+static const int kArchiveDaysThreshold = 90;
+
+// Comparison used when inserting entries into visits vector.
+static bool VisitOccursAfter(const PageVisit& elem1,
+ const PageVisit& elem2) {
+ return elem1.visit_time > elem2.visit_time;
+}
+
+static bool IsMatchingHost(const URLRow& row,
+ const std::string& host_name) {
+ const GURL& url = row.url();
+ if (!url.is_valid() || !url.IsStandard())
+ return false;
+
+ const std::string& spec = url.spec();
+ url_parse::Parsed parsed = url.parsed_for_possibly_invalid_spec();
+ if (!parsed.host.is_nonempty())
+ return false; // Empty host.
+
+ if (parsed.host.len != host_name.size())
+ return false; // Hosts are different lengths, can not match.
+
+ // TODO(brettw) we may want to also match hosts ending with a period, since
+ // these will typically be the same.
+ return strncmp(&spec[parsed.host.begin], host_name.c_str(),
+ host_name.size()) == 0;
+}
+
+// This task is run on a timer so that commits happen at regular intervals
+// so they are batched together. The important thing about this class is that
+// it supports canceling of the task so the reference to the backend will be
+// freed. The problem is that when history is shutting down, there is likely
+// to be one of these commits still pending and holding a reference.
+//
+// The backend can call Cancel to have this task release the reference. The
+// task will still run (if we ever get to processing the event before
+// shutdown), but it will not do anything.
+//
+// Note that this is a refcounted object and is not a task in itself. It should
+// be assigned to a RunnableMethod.
+//
+// TODO(brettw): bug 1165182: This should be replaced with a
+// ScopedRunnableMethodFactory which will handle everything automatically (like
+// we do in ExpireHistoryBackend).
+class CommitLaterTask : public base::RefCounted<CommitLaterTask> {
+ public:
+ explicit CommitLaterTask(HistoryBackend* history_backend)
+ : history_backend_(history_backend) {
+ }
+
+ // The backend will call this function if it is being destroyed so that we
+ // release our reference.
+ void Cancel() {
+ history_backend_ = NULL;
+ }
+
+ void RunCommit() {
+ if (history_backend_.get())
+ history_backend_->Commit();
+ }
+
+ private:
+ scoped_refptr<HistoryBackend> history_backend_;
+};
+
+// Handles querying first the main database, then the full text database if that
+// fails. It will optionally keep track of all URLs seen so duplicates can be
+// eliminated. This is used by the querying sub-functions.
+//
+// TODO(brettw): This class may be able to be simplified or eliminated. After
+// this was written, QueryResults can efficiently look up by URL, so the need
+// for this extra set of previously queried URLs is less important.
+class HistoryBackend::URLQuerier {
+ public:
+ URLQuerier(URLDatabase* main_db, URLDatabase* archived_db, bool track_unique)
+ : main_db_(main_db),
+ archived_db_(archived_db),
+ track_unique_(track_unique) {
+ }
+
+ // When we're tracking unique URLs, returns true if this URL has been
+ // previously queried. Only call when tracking unique URLs.
+ bool HasURL(const GURL& url) {
+ DCHECK(track_unique_);
+ return unique_urls_.find(url) != unique_urls_.end();
+ }
+
+ bool GetRowForURL(const GURL& url, URLRow* row) {
+ if (!main_db_->GetRowForURL(url, row)) {
+ if (!archived_db_ || !archived_db_->GetRowForURL(url, row)) {
+ // This row is neither in the main nor the archived DB.
+ return false;
+ }
+ }
+
+ if (track_unique_)
+ unique_urls_.insert(url);
+ return true;
+ }
+
+ private:
+ URLDatabase* main_db_; // Guaranteed non-NULL.
+ URLDatabase* archived_db_; // Possibly NULL.
+
+ bool track_unique_;
+
+ // When track_unique_ is set, this is updated with every URL seen so far.
+ std::set<GURL> unique_urls_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(URLQuerier);
+};
+
+// HistoryBackend --------------------------------------------------------------
+
+HistoryBackend::HistoryBackend(const std::wstring& history_dir,
+ Delegate* delegate)
+ : delegate_(delegate),
+ history_dir_(history_dir),
+#pragma warning(suppress: 4355) // OK to pass "this" here.
+ expirer_(this),
+ backend_destroy_message_loop_(NULL),
+ recent_redirects_(kMaxRedirectCount),
+ backend_destroy_task_(NULL),
+ segment_queried_(false) {
+}
+
+HistoryBackend::~HistoryBackend() {
+ DCHECK(!scheduled_commit_) << "Deleting without cleanup";
+ ReleaseDBTasks();
+
+ // First close the databases before optionally running the "destroy" task.
+ if (db_.get()) {
+ // Commit the long-running transaction.
+ db_->CommitTransaction();
+ db_.reset();
+ }
+ if (thumbnail_db_.get()) {
+ thumbnail_db_->CommitTransaction();
+ thumbnail_db_.reset();
+ }
+ if (archived_db_.get()) {
+ archived_db_->CommitTransaction();
+ archived_db_.reset();
+ }
+ if (text_database_.get()) {
+ text_database_->CommitTransaction();
+ text_database_.reset();
+ }
+
+ if (backend_destroy_task_) {
+ // Notify an interested party (typically a unit test) that we're done.
+ DCHECK(backend_destroy_message_loop_);
+ backend_destroy_message_loop_->PostTask(FROM_HERE, backend_destroy_task_);
+ }
+}
+
+void HistoryBackend::Init() {
+ DCHECK(!db_.get()) << "Initializing HistoryBackend twice";
+ // In the rare case where the db fails to initialize a dialog may get shown
+ // the blocks the caller, yet allows other messages through. For this reason
+ // we only set db_ to the created database if creation is successful. That
+ // way other methods won't do anything as db_ is still NULL.
+
+ TimeTicks beginning_time = TimeTicks::Now();
+
+ // Compute the file names. Note that the index file can be removed when the
+ // text db manager is finished being hooked up.
+ std::wstring history_name = history_dir_;
+ file_util::AppendToPath(&history_name, chrome::kHistoryFilename);
+ std::wstring thumbnail_name = GetThumbnailFileName();
+ std::wstring archived_name = GetArchivedFileName();
+
+ // History database.
+ db_.reset(new HistoryDatabase());
+ switch (db_->Init(history_name)) {
+ case INIT_OK:
+ break;
+ case INIT_FAILURE:
+ // A NULL db_ will cause all calls on this object to notice this error
+ // and to not continue.
+ LOG(WARNING) << "Unable to initialize history DB.";
+ db_.reset();
+ return;
+ case INIT_TOO_NEW:
+ delegate_->NotifyTooNew();
+ db_.reset();
+ return;
+ default:
+ NOTREACHED();
+ }
+
+ // Fill the in-memory database and send it back to the history service on the
+ // main thread.
+ InMemoryHistoryBackend* mem_backend = new InMemoryHistoryBackend;
+ if (mem_backend->Init(history_name))
+ delegate_->SetInMemoryBackend(mem_backend); // Takes ownership of pointer.
+ else
+ delete mem_backend; // Error case, run without the in-memory DB.
+ db_->BeginExclusiveMode(); // Must be after the mem backend read the data.
+
+ // Full-text database. This has to be first so we can pass it to the
+ // HistoryDatabase for migration.
+ text_database_.reset(new TextDatabaseManager(history_dir_, db_.get()));
+ if (!text_database_->Init()) {
+ LOG(WARNING) << "Text database initialization failed, running without it.";
+ text_database_.reset();
+ }
+
+ // Thumbnail database.
+ thumbnail_db_.reset(new ThumbnailDatabase());
+ if (thumbnail_db_->Init(thumbnail_name) != INIT_OK) {
+ // Unlike the main database, we don't error out when the database is too
+ // new because this error is much less severe. Generally, this shouldn't
+ // happen since the thumbnail and main datbase versions should be in sync.
+ // We'll just continue without thumbnails & favicons in this case or any
+ // other error.
+ LOG(WARNING) << "Could not initialize the thumbnail database.";
+ thumbnail_db_.reset();
+ }
+
+ // Archived database.
+ archived_db_.reset(new ArchivedDatabase());
+ if (!archived_db_->Init(archived_name)) {
+ LOG(WARNING) << "Could not initialize the archived database.";
+ archived_db_.reset();
+ }
+
+ // Tell the expiration module about all the nice databases we made. This must
+ // happen before db_->Init() is called since the callback ForceArchiveHistory
+ // may need to expire stuff.
+ //
+ // *sigh*, this can all be cleaned up when that migration code is removed.
+ // The main DB initialization should intuitively be first (not that it
+ // actually matters) and the expirer should be set last.
+ expirer_.SetDatabases(db_.get(), archived_db_.get(),
+ thumbnail_db_.get(), text_database_.get());
+
+ // Open the long-running transaction.
+ db_->BeginTransaction();
+ if (thumbnail_db_.get())
+ thumbnail_db_->BeginTransaction();
+ if (archived_db_.get())
+ archived_db_->BeginTransaction();
+ if (text_database_.get())
+ text_database_->BeginTransaction();
+
+ // Start expiring old stuff.
+ expirer_.StartArchivingOldStuff(TimeDelta::FromDays(kArchiveDaysThreshold));
+
+ HISTOGRAM_TIMES(L"History.InitTime",
+ TimeTicks::Now() - beginning_time);
+}
+
+void HistoryBackend::SetOnBackendDestroyTask(MessageLoop* message_loop,
+ Task* task) {
+ if (backend_destroy_task_) {
+ DLOG(WARNING) << "Setting more than one destroy task, overriding";
+ delete backend_destroy_task_;
+ }
+ backend_destroy_message_loop_ = message_loop;
+ backend_destroy_task_ = task;
+}
+
+void HistoryBackend::Closing() {
+ // Any scheduled commit will have a reference to us, we must make it
+ // release that reference before we can be destroyed.
+ CancelScheduledCommit();
+
+ // Release our reference to the delegate, this reference will be keeping the
+ // history service alive.
+ delegate_.reset();
+}
+
+void HistoryBackend::NotifyRenderProcessHostDestruction(const void* host) {
+ tracker_.NotifyRenderProcessHostDestruction(host);
+}
+
+std::wstring HistoryBackend::GetThumbnailFileName() const {
+ std::wstring thumbnail_name = history_dir_;
+ file_util::AppendToPath(&thumbnail_name, chrome::kThumbnailsFilename);
+ return thumbnail_name;
+}
+
+std::wstring HistoryBackend::GetArchivedFileName() const {
+ std::wstring archived_name = history_dir_;
+ file_util::AppendToPath(&archived_name, chrome::kArchivedHistoryFilename);
+ return archived_name;
+}
+
+SegmentID HistoryBackend::GetLastSegmentID(VisitID from_visit) {
+ VisitID visit_id = from_visit;
+ while (visit_id) {
+ VisitRow row;
+ if (!db_->GetRowForVisit(visit_id, &row))
+ return 0;
+ if (row.segment_id)
+ return row.segment_id; // Found a visit in this change with a segment.
+
+ // Check the referrer of this visit, if any.
+ visit_id = row.referring_visit;
+ }
+ return 0;
+}
+
+SegmentID HistoryBackend::UpdateSegments(const GURL& url,
+ VisitID from_visit,
+ VisitID visit_id,
+ PageTransition::Type transition_type,
+ const Time ts) {
+ if (!db_.get())
+ return 0;
+
+ // We only consider main frames.
+ if (!PageTransition::IsMainFrame(transition_type))
+ return 0;
+
+ SegmentID segment_id = 0;
+ PageTransition::Type t = PageTransition::StripQualifier(transition_type);
+
+ // Are we at the beginning of a new segment?
+ if (t == PageTransition::TYPED || t == PageTransition::AUTO_BOOKMARK) {
+ // If so, create or get the segment.
+ std::string segment_name = db_->ComputeSegmentName(url);
+ URLID url_id = db_->GetRowForURL(url, NULL);
+ if (!url_id)
+ return 0;
+
+ if (!(segment_id = db_->GetSegmentNamed(segment_name))) {
+ if (!(segment_id = db_->CreateSegment(url_id, segment_name))) {
+ NOTREACHED();
+ return 0;
+ }
+ } else {
+ // Note: if we update an existing segment, we update the url used to
+ // represent that segment in order to minimize stale most visited
+ // images.
+ db_->UpdateSegmentRepresentationURL(segment_id, url_id);
+ }
+ } else {
+ // Note: it is possible there is no segment ID set for this visit chain.
+ // This can happen if the initial navigation wasn't AUTO_BOOKMARK or
+ // TYPED. (For example GENERATED). In this case this visit doesn't count
+ // toward any segment.
+ if (!(segment_id = GetLastSegmentID(from_visit)))
+ return 0;
+ }
+
+ // Set the segment in the visit.
+ if (!db_->SetSegmentID(visit_id, segment_id)) {
+ NOTREACHED();
+ return 0;
+ }
+
+ // Finally, increase the counter for that segment / day.
+ if (!db_->IncreaseSegmentVisitCount(segment_id, ts, 1)) {
+ NOTREACHED();
+ return 0;
+ }
+ return segment_id;
+}
+
+void HistoryBackend::AddPage(scoped_refptr<HistoryAddPageArgs> request) {
+ DLOG(INFO) << "Adding page " << request->url.possibly_invalid_spec();
+
+ if (!db_.get())
+ return;
+
+ // Will be filled with the URL ID and the visit ID of the last addition.
+ std::pair<URLID, VisitID> last_ids(0, tracker_.GetLastVisit(
+ request->id_scope, request->page_id, request->referrer));
+
+ VisitID from_visit_id = last_ids.second;
+
+ // If a redirect chain is given, we expect the last item in that chain to be
+ // the final URL.
+ DCHECK(request->redirects.size() == 0 ||
+ request->redirects.back() == request->url);
+
+ // Avoid duplicating times in the database, at least as long as pages are
+ // added in order. However, we don't want to disallow pages from recording
+ // times earlier than our last_recorded_time_, because someone might set
+ // their machine's clock back.
+ if (last_requested_time_ == request->time) {
+ last_recorded_time_ = last_recorded_time_ + TimeDelta::FromMicroseconds(1);
+ } else {
+ last_requested_time_ = request->time;
+ last_recorded_time_ = last_requested_time_;
+ }
+
+ if (request->redirects.size() <= 1) {
+ // The single entry is both a chain start and end.
+ PageTransition::Type t = request->transition |
+ PageTransition::CHAIN_START | PageTransition::CHAIN_END;
+
+ // No redirect case (one element means just the page itself).
+ last_ids = AddPageVisit(request->url, last_recorded_time_,
+ last_ids.second, t);
+
+ // Update the segment for this visit.
+ UpdateSegments(request->url, from_visit_id, last_ids.second, t,
+ last_recorded_time_);
+ } else {
+ // Redirect case. Add the redirect chain.
+ PageTransition::Type transition =
+ PageTransition::StripQualifier(request->transition);
+
+ PageTransition::Type redirect_info = PageTransition::CHAIN_START;
+
+ if (request->redirects[0].SchemeIs("about")) {
+ // When the redirect source + referrer is "about" we skip it. This
+ // happens when a page opens a new frame/window to about:blank and then
+ // script sets the URL to somewhere else (used to hide the referrer). It
+ // would be nice to keep all these redirects properly but we don't ever
+ // see the initial about:blank load, so we don't know where the
+ // subsequent client redirect came from.
+ //
+ // In this case, we just don't bother hooking up the source of the
+ // redirects, so we remove it.
+ request->redirects.erase(request->redirects.begin());
+ } else if (request->transition & PageTransition::CLIENT_REDIRECT) {
+ redirect_info = PageTransition::CLIENT_REDIRECT;
+ // The first entry in the redirect chain initiated a client redirect.
+ // We don't add this to the database since the referrer is already
+ // there, so we skip over it but change the transition type of the first
+ // transition to client redirect.
+ //
+ // The referrer is invalid when restoring a session that features an
+ // https tab that redirects to a different host or to http. In this
+ // case we don't need to reconnect the new redirect with the existing
+ // chain.
+ if (request->referrer.is_valid()) {
+ DCHECK(request->referrer == request->redirects[0]);
+ request->redirects.erase(request->redirects.begin());
+
+ // Make sure to remove the CHAIN_END marker from the first visit. This
+ // can be called a lot, for example, the page cycler, and most of the
+ // time we won't have changed anything.
+ // TODO(brettw) this should be unit tested.
+ VisitRow visit_row;
+ if (db_->GetRowForVisit(last_ids.second, &visit_row) &&
+ visit_row.transition | PageTransition::CHAIN_END) {
+ visit_row.transition &= ~PageTransition::CHAIN_END;
+ db_->UpdateVisitRow(visit_row);
+ }
+ }
+ }
+
+ for (size_t redirect_index = 0; redirect_index < request->redirects.size();
+ redirect_index++) {
+ PageTransition::Type t = transition | redirect_info;
+
+ // If this is the last transition, add a CHAIN_END marker
+ if (redirect_index == (request->redirects.size() - 1))
+ t = t | PageTransition::CHAIN_END;
+
+ // Record all redirect visits with the same timestamp. We don't display
+ // them anyway, and if we ever decide to, we can reconstruct their order
+ // from the redirect chain.
+ last_ids = AddPageVisit(request->redirects[redirect_index],
+ last_recorded_time_, last_ids.second, t);
+ if (t & PageTransition::CHAIN_START) {
+ // Update the segment for this visit.
+ UpdateSegments(request->redirects[redirect_index],
+ from_visit_id, last_ids.second, t, last_recorded_time_);
+ }
+
+ // Subsequent transitions in the redirect list must all be sever
+ // redirects.
+ redirect_info = PageTransition::SERVER_REDIRECT;
+ }
+
+ // Last, save this redirect chain for later so we can set titles & favicons
+ // on the redirected pages properly. It is indexed by the destination page.
+ recent_redirects_.Put(request->url, request->redirects);
+ }
+
+ // TODO(brettw) bug 1140015: Add an "add page" notification so the history
+ // views can keep in sync.
+
+ // Add the last visit to the tracker so we can get outgoing transitions.
+ // TODO(evanm): Due to http://b/1194536 we lose the referrers of a subframe
+ // navigation anyway, so last_visit_id is always zero for them. But adding
+ // them here confuses main frame history, so we skip them for now.
+ PageTransition::Type transition =
+ PageTransition::StripQualifier(request->transition);
+ if (transition != PageTransition::AUTO_SUBFRAME &&
+ transition != PageTransition::MANUAL_SUBFRAME) {
+ tracker_.AddVisit(request->id_scope, request->page_id, request->url,
+ last_ids.second);
+ }
+
+ if (text_database_.get()) {
+ text_database_->AddPageURL(request->url, last_ids.first, last_ids.second,
+ last_recorded_time_);
+ }
+
+ ScheduleCommit();
+}
+
+std::pair<URLID, VisitID> HistoryBackend::AddPageVisit(
+ const GURL& url,
+ Time time,
+ VisitID referring_visit,
+ PageTransition::Type transition) {
+ // Top-level frame navigations are visible, everything else is hidden
+ bool new_hidden = !PageTransition::IsMainFrame(transition);
+
+ // NOTE: This code must stay in sync with
+ // ExpireHistoryBackend::ExpireURLsForVisits().
+ // TODO(pkasting): http://b/1148304 We shouldn't be marking so many URLs as
+ // typed, which would eliminate the need for this code.
+ int typed_increment = 0;
+ if (PageTransition::StripQualifier(transition) == PageTransition::TYPED &&
+ !PageTransition::IsRedirect(transition))
+ typed_increment = 1;
+
+ // See if this URL is already in the DB.
+ URLRow url_info(url);
+ URLID url_id = db_->GetRowForURL(url, &url_info);
+ if (url_id) {
+ // Update of an existing row.
+ if (PageTransition::StripQualifier(transition) != PageTransition::RELOAD)
+ url_info.set_visit_count(url_info.visit_count() + 1);
+ int old_typed_count = url_info.typed_count();
+ if (typed_increment)
+ url_info.set_typed_count(url_info.typed_count() + typed_increment);
+ url_info.set_last_visit(time);
+
+ // Only allow un-hiding of pages, never hiding.
+ if (!new_hidden)
+ url_info.set_hidden(false);
+
+ db_->UpdateURLRow(url_id, url_info);
+ } else {
+ // Addition of a new row.
+ url_info.set_visit_count(1);
+ url_info.set_typed_count(typed_increment);
+ url_info.set_last_visit(time);
+ url_info.set_hidden(new_hidden);
+
+ url_id = db_->AddURL(url_info);
+ if (!url_id) {
+ NOTREACHED() << "Adding URL failed.";
+ return std::make_pair(0, 0);
+ }
+ url_info.id_ = url_id;
+
+ // We don't actually add the URL to the full text index at this point. It
+ // might be nice to do this so that even if we get no title or body, the
+ // user can search for URL components and get the page.
+ //
+ // However, in most cases, we'll get at least a title and usually contents,
+ // and this add will be redundant, slowing everything down. As a result,
+ // we ignore this edge case.
+ }
+
+ // Add the visit with the time to the database.
+ VisitRow visit_info(url_id, time, referring_visit, transition, 0);
+ VisitID visit_id = db_->AddVisit(&visit_info);
+
+ // Broadcast a notification of the visit.
+ if (visit_id) {
+ URLVisitedDetails* details = new URLVisitedDetails;
+ details->row = url_info;
+ BroadcastNotifications(NOTIFY_HISTORY_URL_VISITED, details);
+ }
+
+ return std::make_pair(url_id, visit_id);
+}
+
+// Note: this method is only for testing purposes.
+void HistoryBackend::AddPagesWithDetails(const std::vector<URLRow>& urls) {
+ if (!db_.get())
+ return;
+
+ URLsModifiedDetails* modified = new URLsModifiedDetails;
+ for (std::vector<URLRow>::const_iterator i = urls.begin();
+ i != urls.end(); ++i) {
+ DCHECK(!i->last_visit().is_null());
+
+ // We will add to either the archived database or the main one depending on
+ // the date of the added visit.
+ URLDatabase* url_database;
+ VisitDatabase* visit_database;
+ if (i->last_visit() < expirer_.GetCurrentArchiveTime()) {
+ if (!archived_db_.get())
+ return; // No archived database to save it to, just forget this.
+ url_database = archived_db_.get();
+ visit_database = archived_db_.get();
+ } else {
+ url_database = db_.get();
+ visit_database = db_.get();
+ }
+
+ URLRow existing_url;
+ URLID url_id = url_database->GetRowForURL(i->url(), &existing_url);
+ if (!url_id) {
+ // Add the page if it doesn't exist.
+ url_id = url_database->AddURL(*i);
+ if (!url_id) {
+ NOTREACHED() << "Could not add row to DB";
+ return;
+ }
+
+ if (i->typed_count() > 0)
+ modified->changed_urls.push_back(*i);
+ }
+
+ // Add the page to the full text index. This function is also used for
+ // importing. Even though we don't have page contents, we can at least
+ // add the title and URL to the index so they can be searched. We don't
+ // bother to delete any already-existing FTS entries for the URL, since
+ // this is normally called on import.
+ //
+ // If you ever import *after* first run (selecting import from the menu),
+ // then these additional entries will "shadow" the originals when querying
+ // for the most recent match only, and the user won't get snippets. This is
+ // a very minor issue, and fixing it will make import slower, so we don't
+ // bother.
+ bool has_indexed = false;
+ if (text_database_.get()) {
+ // We do not have to make it update the visit database, below, we will
+ // create the visit entry with the indexed flag set.
+ has_indexed = text_database_->AddPageData(i->url(), url_id, 0,
+ i->last_visit(),
+ i->title(), std::wstring());
+ }
+
+ // Make up a visit to correspond to that page.
+ VisitRow visit_info(url_id, i->last_visit(), 0,
+ PageTransition::LINK | PageTransition::CHAIN_START |
+ PageTransition::CHAIN_END, 0);
+ visit_info.is_indexed = has_indexed;
+ if (!visit_database->AddVisit(&visit_info)) {
+ NOTREACHED() << "Adding visit failed.";
+ return;
+ }
+ }
+
+ // Broadcast a notification for typed URLs that have been modified. This
+ // will be picked up by the in-memory URL database on the main thread.
+ //
+ // TODO(brettw) bug 1140015: Add an "add page" notification so the history
+ // views can keep in sync.
+ BroadcastNotifications(NOTIFY_HISTORY_TYPED_URLS_MODIFIED, modified);
+
+ ScheduleCommit();
+}
+
+void HistoryBackend::SetPageTitle(const GURL& url,
+ const std::wstring& title) {
+ if (!db_.get())
+ return;
+
+ // Search for recent redirects which should get the same title. We make a
+ // dummy list containing the exact URL visited if there are no redirects so
+ // the processing below can be the same.
+ HistoryService::RedirectList dummy_list;
+ HistoryService::RedirectList* redirects;
+ RedirectCache::iterator iter = recent_redirects_.Get(url);
+ if (iter != recent_redirects_.end()) {
+ redirects = &iter->second;
+
+ // This redirect chain should have the destination URL as the last item.
+ DCHECK(!redirects->empty());
+ DCHECK(redirects->back() == url);
+ } else {
+ // No redirect chain stored, make up one containing the URL we want so we
+ // can use the same logic below.
+ dummy_list.push_back(url);
+ redirects = &dummy_list;
+ }
+
+ bool typed_url_changed = false;
+ std::vector<URLRow> changed_urls;
+ for (size_t i = 0; i < redirects->size(); i++) {
+ URLRow row;
+ URLID row_id = db_->GetRowForURL(redirects->at(i), &row);
+ if (row_id && row.title() != title) {
+ row.set_title(title);
+ db_->UpdateURLRow(row_id, row);
+ changed_urls.push_back(row);
+ if (row.typed_count() > 0)
+ typed_url_changed = true;
+ }
+ }
+
+ // Broadcast notifications for typed URLs that have changed. This will
+ // update the in-memory database.
+ //
+ // TODO(brettw) bug 1140020: Broadcast for all changes (not just typed),
+ // in which case some logic can be removed.
+ if (typed_url_changed) {
+ URLsModifiedDetails* modified =
+ new URLsModifiedDetails;
+ for (size_t i = 0; i < changed_urls.size(); i++) {
+ if (changed_urls[i].typed_count() > 0)
+ modified->changed_urls.push_back(changed_urls[i]);
+ }
+ BroadcastNotifications(NOTIFY_HISTORY_TYPED_URLS_MODIFIED, modified);
+ }
+
+ // Update the full text index.
+ if (text_database_.get())
+ text_database_->AddPageTitle(url, title);
+
+ // Only bother committing if things changed.
+ if (!changed_urls.empty())
+ ScheduleCommit();
+}
+
+void HistoryBackend::IterateURLs(HistoryService::URLEnumerator* iterator) {
+ if (db_.get()) {
+ HistoryDatabase::URLEnumerator e;
+ if (db_->InitURLEnumeratorForEverything(&e)) {
+ URLRow info;
+ while (e.GetNextURL(&info)) {
+ iterator->OnURL(info.url());
+ }
+ iterator->OnComplete(true); // Success.
+ return;
+ }
+ }
+ iterator->OnComplete(false); // Failure.
+}
+
+void HistoryBackend::QueryURL(scoped_refptr<QueryURLRequest> request,
+ const GURL& url,
+ bool want_visits) {
+ if (request->canceled())
+ return;
+
+ bool success = false;
+ URLRow* row = &request->value.a;
+ VisitVector* visits = &request->value.b;
+ if (db_.get()) {
+ if (db_->GetRowForURL(url, row)) {
+ // Have a row.
+ success = true;
+
+ // Optionally query the visits.
+ if (want_visits)
+ db_->GetVisitsForURL(row->id(), visits);
+ }
+ }
+ request->ForwardResult(QueryURLRequest::TupleType(request->handle(), success,
+ row, visits));
+}
+
+// Segment usage ---------------------------------------------------------------
+
+void HistoryBackend::DeleteOldSegmentData() {
+ if (db_.get())
+ db_->DeleteSegmentData(Time::Now() -
+ TimeDelta::FromDays(kSegmentDataRetention));
+}
+
+void HistoryBackend::SetSegmentPresentationIndex(SegmentID segment_id,
+ int index) {
+ if (db_.get())
+ db_->SetSegmentPresentationIndex(segment_id, index);
+}
+
+void HistoryBackend::QuerySegmentUsage(
+ scoped_refptr<QuerySegmentUsageRequest> request,
+ const Time from_time) {
+ if (request->canceled())
+ return;
+
+ if (db_.get()) {
+ db_->QuerySegmentUsage(from_time, &request->value.get());
+
+ // If this is the first time we query segments, invoke
+ // DeleteOldSegmentData asynchronously. We do this to cleanup old
+ // entries.
+ if (!segment_queried_) {
+ segment_queried_ = true;
+ MessageLoop::current()->PostTask(FROM_HERE,
+ NewRunnableMethod(this, &HistoryBackend::DeleteOldSegmentData));
+ }
+ }
+ request->ForwardResult(
+ QuerySegmentUsageRequest::TupleType(request->handle(),
+ &request->value.get()));
+}
+
+// Keyword visits --------------------------------------------------------------
+
+void HistoryBackend::SetKeywordSearchTermsForURL(const GURL& url,
+ TemplateURL::IDType keyword_id,
+ const std::wstring& term) {
+ if (!db_.get())
+ return;
+
+ // Get the ID for this URL.
+ URLRow url_row;
+ if (!db_->GetRowForURL(url, &url_row)) {
+ // There is a small possibility the url was deleted before the keyword
+ // was added. Ignore the request.
+ return;
+ }
+
+ db_->SetKeywordSearchTermsForURL(url_row.id(), keyword_id, term);
+ ScheduleCommit();
+}
+
+void HistoryBackend::DeleteAllSearchTermsForKeyword(
+ TemplateURL::IDType keyword_id) {
+ if (!db_.get())
+ return;
+
+ db_->DeleteAllSearchTermsForKeyword(keyword_id);
+ // TODO(sky): bug 1168470. Need to move from archive dbs too.
+ ScheduleCommit();
+}
+
+void HistoryBackend::GetMostRecentKeywordSearchTerms(
+ scoped_refptr<GetMostRecentKeywordSearchTermsRequest> request,
+ TemplateURL::IDType keyword_id,
+ const std::wstring& prefix,
+ int max_count) {
+ if (request->canceled())
+ return;
+
+ if (db_.get()) {
+ db_->GetMostRecentKeywordSearchTerms(keyword_id, prefix, max_count,
+ &(request->value));
+ }
+ request->ForwardResult(
+ GetMostRecentKeywordSearchTermsRequest::TupleType(request->handle(),
+ &request->value));
+}
+
+// Downloads -------------------------------------------------------------------
+
+// Get all the download entries from the database.
+void HistoryBackend::QueryDownloads(
+ scoped_refptr<DownloadQueryRequest> request) {
+ if (request->canceled())
+ return;
+ if (db_.get())
+ db_->QueryDownloads(&request->value);
+ request->ForwardResult(DownloadQueryRequest::TupleType(&request->value));
+}
+
+// Update a particular download entry.
+void HistoryBackend::UpdateDownload(int64 received_bytes,
+ int32 state,
+ int64 db_handle) {
+ if (db_.get())
+ db_->UpdateDownload(received_bytes, state, db_handle);
+}
+
+// Create a new download entry and pass back the db_handle to it.
+void HistoryBackend::CreateDownload(
+ scoped_refptr<DownloadCreateRequest> request,
+ const DownloadCreateInfo& create_info) {
+ int64 db_handle = 0;
+ if (!request->canceled()) {
+ if (db_.get())
+ db_handle = db_->CreateDownload(create_info);
+ request->ForwardResult(DownloadCreateRequest::TupleType(create_info,
+ db_handle));
+ }
+}
+
+void HistoryBackend::RemoveDownload(int64 db_handle) {
+ if (db_.get())
+ db_->RemoveDownload(db_handle);
+}
+
+void HistoryBackend::RemoveDownloadsBetween(const Time remove_begin,
+ const Time remove_end) {
+ if (db_.get())
+ db_->RemoveDownloadsBetween(remove_begin, remove_end);
+}
+
+void HistoryBackend::SearchDownloads(
+ scoped_refptr<DownloadSearchRequest> request,
+ const std::wstring& search_text) {
+ if (request->canceled())
+ return;
+ if (db_.get())
+ db_->SearchDownloads(&request->value, search_text);
+ request->ForwardResult(DownloadSearchRequest::TupleType(request->handle(),
+ &request->value));
+}
+
+void HistoryBackend::QueryHistory(scoped_refptr<QueryHistoryRequest> request,
+ const std::wstring& text_query,
+ const QueryOptions& options) {
+ if (request->canceled())
+ return;
+
+ TimeTicks beginning_time = TimeTicks::Now();
+
+ if (db_.get()) {
+ if (text_query.empty()) {
+ if (options.begin_time.is_null() && options.end_time.is_null() &&
+ options.only_starred && options.max_count == 0) {
+ DLOG(WARNING) << "Querying for all bookmarks. You should probably use "
+ "the dedicated starring functions which will also report unvisited "
+ "bookmarks and will be faster.";
+ // If this case is needed and the starring functions aren't right, we
+ // can optimize the case where we're just querying for starred URLs
+ // and remove the warning.
+ }
+
+ // Basic history query for the main database.
+ QueryHistoryBasic(db_.get(), db_.get(), options, &request->value);
+
+ // Now query the archived database. This is a bit tricky because we don't
+ // want to query it if the queried time range isn't going to find anything
+ // in it.
+ // TODO(brettw) bug 1171036: do blimpie querying for the archived database
+ // as well.
+ // if (archived_db_.get() &&
+ // expirer_.GetCurrentArchiveTime() - TimeDelta::FromDays(7)) {
+ } else {
+ // Full text history query.
+ QueryHistoryFTS(text_query, options, &request->value);
+ }
+ }
+
+ request->ForwardResult(QueryHistoryRequest::TupleType(request->handle(),
+ &request->value));
+
+ HISTOGRAM_TIMES(L"History.QueryHistory",
+ TimeTicks::Now() - beginning_time);
+}
+
+// Basic time-based querying of history.
+void HistoryBackend::QueryHistoryBasic(URLDatabase* url_db,
+ VisitDatabase* visit_db,
+ const QueryOptions& options,
+ QueryResults* result) {
+ // First get all visits.
+ VisitVector visits;
+ visit_db->GetVisibleVisitsInRange(options.begin_time, options.end_time,
+ options.most_recent_visit_only,
+ options.max_count, &visits);
+ DCHECK(options.max_count == 0 ||
+ static_cast<int>(visits.size()) <= options.max_count);
+
+ // Now add them and the URL rows to the results.
+ URLResult url_result;
+ for (size_t i = 0; i < visits.size(); i++) {
+ const VisitRow visit = visits[i];
+
+ // Add a result row for this visit, get the URL info from the DB.
+ if (!url_db->GetURLRow(visit.url_id, &url_result))
+ continue; // DB out of sync and URL doesn't exist, try to recover.
+ if (!url_result.url().is_valid())
+ continue; // Don't report invalid URLs in case of corruption.
+
+ // The archived database may be out of sync with respect to starring,
+ // titles, last visit date, etc. Therefore, we query the main DB if the
+ // current URL database is not the main one.
+ if (url_db == db_.get()) {
+ // Currently querying the archived DB, update with the main database to
+ // catch any interesting stuff. This will update it if it exists in the
+ // main DB, and do nothing otherwise.
+ db_->GetRowForURL(url_result.url(), &url_result);
+ } else {
+ // URLs not in the main DB can't be starred, reset this just in case.
+ url_result.set_star_id(0);
+ }
+
+ if (!url_result.starred() && options.only_starred)
+ continue; // Want non-starred items filtered out.
+
+ url_result.set_visit_time(visit.visit_time);
+
+ // We don't set any of the query-specific parts of the URLResult, since
+ // snippets and stuff don't apply to basic querying.
+ result->AppendURLBySwapping(&url_result);
+ }
+}
+
+void HistoryBackend::QueryStarredEntriesByText(
+ URLQuerier* querier,
+ const std::wstring& text_query,
+ const QueryOptions& options,
+ QueryResults* results) {
+ // Collect our prepends so we can bulk add them at the end. It is more
+ // efficient to bluk prepend the URLs than to do one at a time.
+ QueryResults our_results;
+
+ // We can use only the main DB (not archived) for this operation since we know
+ // that all currently starred URLs will be in the main DB.
+ std::set<URLID> ids;
+ db_->GetURLsForTitlesMatching(text_query, &ids);
+
+ VisitRow visit;
+ PageVisit page_visit;
+ URLResult url_result;
+ for (std::set<URLID>::iterator i = ids.begin(); i != ids.end(); ++i) {
+ // Turn the ID (associated with the main DB) into a visit row.
+ if (!db_->GetURLRow(*i, &url_result))
+ continue; // Not found, some crazy error.
+
+ // Make sure we haven't already reported this URL and don't add it if so.
+ if (querier->HasURL(url_result.url()))
+ continue;
+
+ // Consistency check, all URLs should be valid and starred.
+ if (!url_result.url().is_valid() || !url_result.starred())
+ continue;
+
+ db_->GetStarredEntry(url_result.star_id(), &url_result.starred_entry_);
+
+ // Use the last visit time as the visit time for this result.
+ // TODO(brettw) when we are not querying for the most recent visit only,
+ // we should get all the visits in the time range for the given URL and add
+ // separate results for each of them. Until then, just treat as unique:
+ //
+ // Just add this one visit for the last time they visited it, except for
+ // starred only queries which have no visit times.
+ if (options.only_starred) {
+ url_result.set_visit_time(Time());
+ } else {
+ url_result.set_visit_time(url_result.last_visit());
+ }
+ our_results.AppendURLBySwapping(&url_result);
+ }
+
+ // Now prepend all of the bookmark matches we found. We do this by appending
+ // the old values to the new ones and swapping the results.
+ if (our_results.size() > 0) {
+ our_results.AppendResultsBySwapping(results,
+ options.most_recent_visit_only);
+ our_results.Swap(results);
+ }
+}
+
+void HistoryBackend::QueryHistoryFTS(const std::wstring& text_query,
+ const QueryOptions& options,
+ QueryResults* result) {
+ if (!text_database_.get())
+ return;
+
+ // Full text query, first get all the FTS results in the time range.
+ std::vector<TextDatabase::Match> fts_matches;
+ Time first_time_searched;
+ text_database_->GetTextMatches(text_query, options,
+ &fts_matches, &first_time_searched);
+
+ URLQuerier querier(db_.get(), archived_db_.get(), true);
+
+ // Now get the row and visit information for each one, optionally
+ // filtering on starred items.
+ URLResult url_result; // Declare outside loop to prevent re-construction.
+ for (size_t i = 0; i < fts_matches.size(); i++) {
+ if (options.max_count != 0 &&
+ static_cast<int>(result->size()) >= options.max_count)
+ break; // Got too many items.
+
+ // Get the URL, querying the main and archived databases as necessary. If
+ // this is not found, the history and full text search databases are out
+ // of sync and we give up with this result.
+ if (!querier.GetRowForURL(fts_matches[i].url, &url_result))
+ continue;
+
+ if (!url_result.url().is_valid())
+ continue; // Don't report invalid URLs in case of corruption.
+ if (options.only_starred && !url_result.star_id())
+ continue; // Don't add this unstarred item.
+
+ // Copy over the FTS stuff that the URLDatabase doesn't know about.
+ // We do this with swap() to avoid copying, since we know we don't
+ // need the original any more. Note that we override the title with the
+ // one from FTS, since that will match the title_match_positions (the
+ // FTS title and the history DB title may differ).
+ url_result.set_title(fts_matches[i].title);
+ url_result.title_match_positions_.swap(
+ fts_matches[i].title_match_positions);
+ url_result.snippet_.Swap(&fts_matches[i].snippet);
+
+ // The visit time also comes from the full text search database. Since it
+ // has the time, we can avoid an extra query of the visits table.
+ url_result.set_visit_time(fts_matches[i].time);
+
+ if (options.only_starred) {
+ // When querying for starred pages fetch the starred entry.
+ DCHECK(url_result.star_id());
+ db_->GetStarredEntry(url_result.star_id(), &url_result.starred_entry_);
+ } else {
+ url_result.ResetStarredEntry();
+ }
+
+ // Add it to the vector, this will clear our |url_row| object as a
+ // result of the swap.
+ result->AppendURLBySwapping(&url_result);
+ }
+
+ if (options.include_all_starred)
+ QueryStarredEntriesByText(&querier, text_query, options, result);
+}
+
+// Frontend to GetMostRecentRedirectsFrom from the history thread.
+void HistoryBackend::QueryRedirectsFrom(
+ scoped_refptr<QueryRedirectsRequest> request,
+ const GURL& url) {
+ if (request->canceled())
+ return;
+ bool success = GetMostRecentRedirectsFrom(url, &request->value);
+ request->ForwardResult(QueryRedirectsRequest::TupleType(
+ request->handle(), url, success, &request->value));
+}
+
+void HistoryBackend::GetVisitCountToHost(
+ scoped_refptr<GetVisitCountToHostRequest> request,
+ const GURL& url) {
+ if (request->canceled())
+ return;
+ int count = 0;
+ Time first_visit;
+ const bool success = (db_.get() && db_->GetVisitCountToHost(url, &count,
+ &first_visit));
+ request->ForwardResult(GetVisitCountToHostRequest::TupleType(
+ request->handle(), success, count, first_visit));
+}
+
+void HistoryBackend::GetRedirectsFromSpecificVisit(
+ VisitID cur_visit, HistoryService::RedirectList* redirects) {
+ // Follow any redirects from the given visit and add them to the list.
+ // It *should* be impossible to get a circular chain here, but we check
+ // just in case to avoid infinite loops.
+ GURL cur_url;
+ std::set<VisitID> visit_set;
+ visit_set.insert(cur_visit);
+ while (db_->GetRedirectFromVisit(cur_visit, &cur_visit, &cur_url)) {
+ if (visit_set.find(cur_visit) != visit_set.end()) {
+ NOTREACHED() << "Loop in visit chain, giving up";
+ return;
+ }
+ visit_set.insert(cur_visit);
+ redirects->push_back(cur_url);
+ }
+}
+
+bool HistoryBackend::GetMostRecentRedirectsFrom(
+ const GURL& from_url,
+ HistoryService::RedirectList* redirects) {
+ redirects->clear();
+ if (!db_.get())
+ return false;
+
+ URLID from_url_id = db_->GetRowForURL(from_url, NULL);
+ VisitID cur_visit = db_->GetMostRecentVisitForURL(from_url_id, NULL);
+ if (!cur_visit)
+ return false; // No visits for URL.
+
+ GetRedirectsFromSpecificVisit(cur_visit, redirects);
+ return true;
+}
+
+void HistoryBackend::ScheduleAutocomplete(HistoryURLProvider* provider,
+ HistoryURLProviderParams* params) {
+ // ExecuteWithDB should handle the NULL database case.
+ provider->ExecuteWithDB(this, db_.get(), params);
+}
+
+void HistoryBackend::SetPageContents(const GURL& url,
+ const std::wstring& contents) {
+ // This is histogrammed in the text database manager.
+ if (!text_database_.get())
+ return;
+ text_database_->AddPageContents(url, contents);
+}
+
+void HistoryBackend::SetPageThumbnail(
+ const GURL& url,
+ const SkBitmap& thumbnail,
+ const ThumbnailScore& score) {
+ if (!db_.get() || !thumbnail_db_.get())
+ return;
+
+ URLID url_id = db_->GetRowForURL(url, NULL);
+ if (url_id)
+ thumbnail_db_->SetPageThumbnail(url_id, thumbnail, score);
+ ScheduleCommit();
+}
+
+void HistoryBackend::GetPageThumbnail(
+ scoped_refptr<GetPageThumbnailRequest> request,
+ const GURL& page_url) {
+ if (request->canceled())
+ return;
+
+ scoped_refptr<RefCountedBytes> data;
+ GetPageThumbnailDirectly(page_url, &data);
+
+ request->ForwardResult(GetPageThumbnailRequest::TupleType(
+ request->handle(), data));
+}
+
+void HistoryBackend::GetPageThumbnailDirectly(
+ const GURL& page_url,
+ scoped_refptr<RefCountedBytes>* data) {
+ if (thumbnail_db_.get()) {
+ *data = new RefCountedBytes;
+
+ // Time the result.
+ TimeTicks beginning_time = TimeTicks::Now();
+
+ HistoryService::RedirectList redirects;
+ URLID url_id;
+ bool success = false;
+
+ // If there are some redirects, try to get a thumbnail from the last
+ // redirect destination.
+ if (GetMostRecentRedirectsFrom(page_url, &redirects) &&
+ !redirects.empty()) {
+ if ((url_id = db_->GetRowForURL(redirects.back(), NULL)))
+ success = thumbnail_db_->GetPageThumbnail(url_id, &(*data)->data);
+ }
+
+ // If we don't have a thumbnail from redirects, try the URL directly.
+ if (!success) {
+ if ((url_id = db_->GetRowForURL(page_url, NULL)))
+ success = thumbnail_db_->GetPageThumbnail(url_id, &(*data)->data);
+ }
+
+ // In this rare case, we start to mine the older redirect sessions
+ // from the visit table to try to find a thumbnail.
+ if (!success) {
+ success = GetThumbnailFromOlderRedirect(page_url, &(*data)->data);
+ }
+
+ if (!success)
+ *data = NULL; // This will tell the callback there was an error.
+
+ HISTOGRAM_TIMES(L"History.GetPageThumbnail",
+ TimeTicks::Now() - beginning_time);
+ }
+}
+
+bool HistoryBackend::GetThumbnailFromOlderRedirect(
+ const GURL& page_url,
+ std::vector<unsigned char>* data) {
+ // Look at a few previous visit sessions.
+ VisitVector older_sessions;
+ URLID page_url_id = db_->GetRowForURL(page_url, NULL);
+ static const int kVisitsToSearchForThumbnail = 4;
+ db_->GetMostRecentVisitsForURL(
+ page_url_id, kVisitsToSearchForThumbnail, &older_sessions);
+
+ // Iterate across all those previous visits, and see if any of the
+ // final destinations of those redirect chains have a good thumbnail
+ // for us.
+ bool success = false;
+ for (VisitVector::const_iterator it = older_sessions.begin();
+ !success && it != older_sessions.end(); ++it) {
+ HistoryService::RedirectList redirects;
+ if (it->visit_id) {
+ GetRedirectsFromSpecificVisit(it->visit_id, &redirects);
+
+ if (!redirects.empty()) {
+ URLID url_id;
+ if ((url_id = db_->GetRowForURL(redirects.back(), NULL)))
+ success = thumbnail_db_->GetPageThumbnail(url_id, data);
+ }
+ }
+ }
+
+ return success;
+}
+
+void HistoryBackend::GetFavIcon(scoped_refptr<GetFavIconRequest> request,
+ const GURL& icon_url) {
+ UpdateFavIconMappingAndFetchImpl(NULL, icon_url, request);
+}
+
+void HistoryBackend::UpdateFavIconMappingAndFetch(
+ scoped_refptr<GetFavIconRequest> request,
+ const GURL& page_url,
+ const GURL& icon_url) {
+ UpdateFavIconMappingAndFetchImpl(&page_url, icon_url, request);
+}
+
+void HistoryBackend::SetFavIconOutOfDateForPage(const GURL& page_url) {
+ if (!thumbnail_db_.get() || !db_.get())
+ return;
+
+ URLRow url_row;
+ URLID url_id = db_->GetRowForURL(page_url, &url_row);
+ if (!url_id || !url_row.favicon_id())
+ return;
+
+ thumbnail_db_->SetFavIconLastUpdateTime(url_row.favicon_id(), Time());
+ ScheduleCommit();
+}
+
+void HistoryBackend::SetImportedFavicons(
+ const std::vector<ImportedFavIconUsage>& favicon_usage) {
+ if (!db_.get() || !thumbnail_db_.get())
+ return;
+
+ Time now = Time::Now();
+
+ // Track all starred URLs that had their favicons set or updated.
+ std::set<GURL> starred_favicons_changed;
+
+ for (size_t i = 0; i < favicon_usage.size(); i++) {
+ FavIconID favicon_id = thumbnail_db_->GetFavIconIDForFavIconURL(
+ favicon_usage[i].favicon_url);
+ if (!favicon_id) {
+ // This favicon doesn't exist yet, so we create it using the given data.
+ favicon_id = thumbnail_db_->AddFavIcon(favicon_usage[i].favicon_url);
+ if (!favicon_id)
+ continue; // Unable to add the favicon.
+ thumbnail_db_->SetFavIcon(favicon_id, favicon_usage[i].png_data, now);
+ }
+
+ // Save the mapping from all the URLs to the favicon.
+ for (std::set<GURL>::const_iterator url = favicon_usage[i].urls.begin();
+ url != favicon_usage[i].urls.end(); ++url) {
+ URLRow url_row;
+ if (!db_->GetRowForURL(*url, &url_row) ||
+ url_row.favicon_id() == favicon_id)
+ continue; // Don't set favicons for unknown URLs.
+ url_row.set_favicon_id(favicon_id);
+ db_->UpdateURLRow(url_row.id(), url_row);
+
+ if (url_row.starred())
+ starred_favicons_changed.insert(*url);
+ }
+ }
+
+ if (!starred_favicons_changed.empty()) {
+ // Send the notification about the changed favicons for starred URLs.
+ FavIconChangeDetails* changed_details = new FavIconChangeDetails;
+ changed_details->urls.swap(starred_favicons_changed);
+ BroadcastNotifications(NOTIFY_STARRED_FAVICON_CHANGED, changed_details);
+ }
+}
+
+void HistoryBackend::UpdateFavIconMappingAndFetchImpl(
+ const GURL* page_url,
+ const GURL& icon_url,
+ scoped_refptr<GetFavIconRequest> request) {
+ if (request->canceled())
+ return;
+
+ bool know_favicon = false;
+ bool expired = true;
+ scoped_refptr<RefCountedBytes> data;
+
+ if (thumbnail_db_.get()) {
+ const FavIconID favicon_id =
+ thumbnail_db_->GetFavIconIDForFavIconURL(icon_url);
+ if (favicon_id) {
+ data = new RefCountedBytes;
+ know_favicon = true;
+ Time last_updated;
+ if (thumbnail_db_->GetFavIcon(favicon_id, &last_updated, &data->data,
+ NULL)) {
+ expired = (Time::Now() - last_updated) >
+ TimeDelta::FromDays(kFavIconRefetchDays);
+ }
+
+ if (page_url)
+ SetFavIconMapping(*page_url, favicon_id);
+ }
+ // else case, haven't cached entry yet. Caller is responsible for
+ // downloading the favicon and invoking SetFavIcon.
+ }
+ request->ForwardResult(GetFavIconRequest::TupleType(
+ request->handle(), know_favicon, data, expired,
+ icon_url));
+}
+
+void HistoryBackend::GetFavIconForURL(
+ scoped_refptr<GetFavIconRequest> request,
+ const GURL& page_url) {
+ if (request->canceled())
+ return;
+
+ bool know_favicon = false;
+ bool expired = false;
+ GURL icon_url;
+
+ scoped_refptr<RefCountedBytes> data;
+
+ if (db_.get() && thumbnail_db_.get()) {
+ // Time the query.
+ TimeTicks beginning_time = TimeTicks::Now();
+
+ URLRow url_info;
+ data = new RefCountedBytes;
+ Time last_updated;
+ if (db_->GetRowForURL(page_url, &url_info) && url_info.favicon_id() &&
+ thumbnail_db_->GetFavIcon(url_info.favicon_id(), &last_updated,
+ &data->data, &icon_url)) {
+ know_favicon = true;
+ expired = (Time::Now() - last_updated) >
+ TimeDelta::FromDays(kFavIconRefetchDays);
+ }
+
+ HISTOGRAM_TIMES(L"History.GetFavIconForURL",
+ TimeTicks::Now() - beginning_time);
+ }
+
+ request->ForwardResult(
+ GetFavIconRequest::TupleType(request->handle(), know_favicon, data,
+ expired, icon_url));
+}
+
+void HistoryBackend::SetFavIcon(
+ const GURL& page_url,
+ const GURL& icon_url,
+ scoped_refptr<RefCountedBytes> data) {
+ DCHECK(data.get());
+ if (!thumbnail_db_.get() || !db_.get())
+ return;
+
+ FavIconID id = thumbnail_db_->GetFavIconIDForFavIconURL(icon_url);
+ if (!id)
+ id = thumbnail_db_->AddFavIcon(icon_url);
+
+ // Set the image data.
+ thumbnail_db_->SetFavIcon(id, data->data, Time::Now());
+
+ SetFavIconMapping(page_url, id);
+}
+
+void HistoryBackend::SetFavIconMapping(const GURL& page_url,
+ FavIconID id) {
+ // Find all the pages whose favicons we should set, we want to set it for
+ // all the pages in the redirect chain if it redirected.
+ HistoryService::RedirectList dummy_list;
+ HistoryService::RedirectList* redirects;
+ RedirectCache::iterator iter = recent_redirects_.Get(page_url);
+ if (iter != recent_redirects_.end()) {
+ redirects = &iter->second;
+
+ // This redirect chain should have the destination URL as the last item.
+ DCHECK(!redirects->empty());
+ DCHECK(redirects->back() == page_url);
+ } else {
+ // No redirect chain stored, make up one containing the URL we want to we
+ // can use the same logic below.
+ dummy_list.push_back(page_url);
+ redirects = &dummy_list;
+ }
+
+ std::set<GURL> starred_favicons_changed;
+
+ // Save page <-> favicon association.
+ for (HistoryService::RedirectList::const_iterator i(redirects->begin());
+ i != redirects->end(); ++i) {
+ URLRow row;
+ if (!db_->GetRowForURL(*i, &row) || row.favicon_id() == id)
+ continue;
+
+ FavIconID old_id = row.favicon_id();
+ if (old_id == id)
+ continue;
+ row.set_favicon_id(id);
+ db_->UpdateURLRow(row.id(), row);
+
+ if (old_id) {
+ // The page's favicon ID changed. This means that the one we just
+ // changed from could have been orphaned, and we need to re-check it.
+ // This is not super fast, but this case will get triggered rarely,
+ // since normally a page will always map to the same favicon ID. It
+ // will mostly happen for favicons we import.
+ if (!db_->IsFavIconUsed(old_id) && thumbnail_db_.get())
+ thumbnail_db_->DeleteFavIcon(old_id);
+ }
+
+ if (row.starred())
+ starred_favicons_changed.insert(row.url());
+ }
+
+ if (!starred_favicons_changed.empty()) {
+ // Send the notification about the changed favicons for starred URLs.
+ FavIconChangeDetails* changed_details = new FavIconChangeDetails;
+ changed_details->urls.swap(starred_favicons_changed);
+ BroadcastNotifications(NOTIFY_STARRED_FAVICON_CHANGED, changed_details);
+ }
+
+ ScheduleCommit();
+}
+
+void HistoryBackend::GetAllStarredEntries(
+ scoped_refptr<GetStarredEntriesRequest> request) {
+ if (request->canceled())
+ return;
+ // Only query for the entries if the starred table is valid. If the starred
+ // table isn't valid, we may get back garbage which could cause the UI grief.
+ //
+ // TODO(sky): bug 1207654: this is temporary, the UI should really query for
+ // valid state than avoid GetAllStarredEntries if not valid.
+ if (db_.get() && db_->is_starred_valid())
+ db_->GetStarredEntries(0, &(request->value));
+ request->ForwardResult(
+ GetStarredEntriesRequest::TupleType(request->handle(),
+ &(request->value)));
+}
+
+void HistoryBackend::UpdateStarredEntry(const StarredEntry& new_entry) {
+ if (!db_.get())
+ return;
+
+ StarredEntry resulting_entry = new_entry;
+ if (!db_->UpdateStarredEntry(&resulting_entry) || !delegate_.get())
+ return;
+
+ ScheduleCommit();
+
+ // Send out notification that the star entry changed.
+ StarredEntryDetails* entry_details = new StarredEntryDetails();
+ entry_details->entry = resulting_entry;
+ BroadcastNotifications(NOTIFY_STAR_ENTRY_CHANGED, entry_details);
+}
+
+void HistoryBackend::CreateStarredEntry(
+ scoped_refptr<CreateStarredEntryRequest> request,
+ const StarredEntry& entry) {
+ // This method explicitly allows request to be NULL.
+ if (request.get() && request->canceled())
+ return;
+
+ StarID id = 0;
+ StarredEntry resulting_entry(entry);
+ if (db_.get()) {
+ if (entry.type == StarredEntry::USER_GROUP) {
+ id = db_->CreateStarredEntry(&resulting_entry);
+ if (id) {
+ // Broadcast group created notifications.
+ StarredEntryDetails* entry_details = new StarredEntryDetails;
+ entry_details->entry = resulting_entry;
+ BroadcastNotifications(NOTIFY_STAR_GROUP_CREATED, entry_details);
+ }
+ } else if (entry.type == StarredEntry::URL) {
+ // Currently, we only allow one starred entry for this URL. Therefore, we
+ // check for an existing starred entry for this URL and update it if it
+ // exists.
+ if (!db_->GetStarIDForEntry(resulting_entry)) {
+ // Adding a new starred URL.
+ id = db_->CreateStarredEntry(&resulting_entry);
+
+ // Broadcast starred notification.
+ URLsStarredDetails* details = new URLsStarredDetails(true);
+ details->changed_urls.insert(resulting_entry.url);
+ details->star_entries.push_back(resulting_entry);
+ BroadcastNotifications(NOTIFY_URLS_STARRED, details);
+ } else {
+ // Updating an existing one.
+ db_->UpdateStarredEntry(&resulting_entry);
+
+ // Broadcast starred update notification.
+ StarredEntryDetails* entry_details = new StarredEntryDetails;
+ entry_details->entry = resulting_entry;
+ BroadcastNotifications(NOTIFY_STAR_ENTRY_CHANGED, entry_details);
+ }
+ } else {
+ NOTREACHED();
+ }
+ }
+
+ ScheduleCommit();
+
+ if (request.get()) {
+ request->ForwardResult(
+ CreateStarredEntryRequest::TupleType(request->handle(), id));
+ }
+}
+
+void HistoryBackend::DeleteStarredGroup(UIStarID group_id) {
+ if (!db_.get())
+ return;
+
+ DeleteStarredEntry(db_->GetStarIDForGroupID(group_id));
+}
+
+void HistoryBackend::DeleteStarredURL(const GURL& url) {
+ if (!db_.get())
+ return;
+
+ history::StarredEntry entry;
+ entry.url = url;
+ DeleteStarredEntry(db_->GetStarIDForEntry(entry));
+}
+
+void HistoryBackend::DeleteStarredEntry(history::StarID star_id) {
+ if (!star_id) {
+ NOTREACHED() << "Deleting a nonexistent entry";
+ return;
+ }
+
+ // Delete the entry.
+ URLsStarredDetails* details = new URLsStarredDetails(false);
+ db_->DeleteStarredEntry(star_id, &details->changed_urls,
+ &details->star_entries);
+
+ ScheduleCommit();
+
+ BroadcastNotifications(NOTIFY_URLS_STARRED, details);
+}
+
+void HistoryBackend::GetMostRecentStarredEntries(
+ scoped_refptr<GetMostRecentStarredEntriesRequest> request,
+ int max_count) {
+ if (request->canceled())
+ return;
+ if (db_.get())
+ db_->GetMostRecentStarredEntries(max_count, &(request->value));
+ request->ForwardResult(
+ GetMostRecentStarredEntriesRequest::TupleType(request->handle(),
+ &(request->value)));
+}
+
+void HistoryBackend::Commit() {
+ if (!db_.get())
+ return;
+
+ // Note that a commit may not actually have been scheduled if a caller
+ // explicitly calls this instead of using ScheduleCommit. Likewise, we
+ // may reset the flag written by a pending commit. But this is OK! It
+ // will merely cause extra commits (which is kind of the idea). We
+ // could optimize more for this case (we may get two extra commits in
+ // some cases) but it hasn't been important yet.
+ CancelScheduledCommit();
+
+ db_->CommitTransaction();
+ DCHECK(db_->transaction_nesting() == 0) << "Somebody left a transaction open";
+ db_->BeginTransaction();
+
+ if (thumbnail_db_.get()) {
+ thumbnail_db_->CommitTransaction();
+ DCHECK(thumbnail_db_->transaction_nesting() == 0) <<
+ "Somebody left a transaction open";
+ thumbnail_db_->BeginTransaction();
+ }
+
+ if (archived_db_.get()) {
+ archived_db_->CommitTransaction();
+ archived_db_->BeginTransaction();
+ }
+
+ if (text_database_.get()) {
+ text_database_->CommitTransaction();
+ text_database_->BeginTransaction();
+ }
+}
+
+void HistoryBackend::ScheduleCommit() {
+ if (scheduled_commit_.get())
+ return;
+ scheduled_commit_ = new CommitLaterTask(this);
+ MessageLoop::current()->PostDelayedTask(FROM_HERE,
+ NewRunnableMethod(scheduled_commit_.get(),
+ &CommitLaterTask::RunCommit),
+ kCommitIntervalMs);
+}
+
+void HistoryBackend::CancelScheduledCommit() {
+ if (scheduled_commit_) {
+ scheduled_commit_->Cancel();
+ scheduled_commit_ = NULL;
+ }
+}
+
+void HistoryBackend::ProcessDBTaskImpl() {
+ if (!db_.get()) {
+ // db went away, release all the refs.
+ ReleaseDBTasks();
+ return;
+ }
+
+ // Remove any canceled tasks.
+ while (!db_task_requests_.empty() && db_task_requests_.front()->canceled()) {
+ db_task_requests_.front()->Release();
+ db_task_requests_.pop_front();
+ }
+ if (db_task_requests_.empty())
+ return;
+
+ // Run the first task.
+ HistoryDBTaskRequest* request = db_task_requests_.front();
+ db_task_requests_.pop_front();
+ if (request->value->RunOnDBThread(this, db_.get())) {
+ // The task is done. Notify the callback.
+ request->ForwardResult(HistoryDBTaskRequest::TupleType());
+ // We AddRef'd the request before adding, need to release it now.
+ request->Release();
+ } else {
+ // Tasks wants to run some more. Schedule it at the end of current tasks.
+ db_task_requests_.push_back(request);
+ // And process it after an invoke later.
+ MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(
+ this, &HistoryBackend::ProcessDBTaskImpl));
+ }
+}
+
+void HistoryBackend::ReleaseDBTasks() {
+ for (std::list<HistoryDBTaskRequest*>::iterator i =
+ db_task_requests_.begin(); i != db_task_requests_.end(); ++i) {
+ (*i)->Release();
+ }
+ db_task_requests_.clear();
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//
+// Generic operations
+//
+////////////////////////////////////////////////////////////////////////////////
+
+void HistoryBackend::DeleteURL(const GURL& url) {
+ expirer_.DeleteURL(url);
+
+ // Force a commit, if the user is deleting something for privacy reasons, we
+ // want to get it on disk ASAP.
+ Commit();
+}
+
+void HistoryBackend::ExpireHistoryBetween(
+ scoped_refptr<ExpireHistoryRequest> request,
+ Time begin_time,
+ Time end_time) {
+ if (request->canceled())
+ return;
+
+ if (db_.get()) {
+ if (begin_time.is_null() && end_time.is_null()) {
+ // Special case deleting all history so it can be faster and to reduce the
+ // possibility of an information leak.
+ DeleteAllHistory();
+ } else {
+ // Clearing parts of history, have the expirer do the depend
+ expirer_.ExpireHistoryBetween(begin_time, end_time);
+
+ // Force a commit, if the user is deleting something for privacy reasons,
+ // we want to get it on disk ASAP.
+ Commit();
+ }
+ }
+
+ request->ForwardResult(ExpireHistoryRequest::TupleType());
+}
+
+void HistoryBackend::ProcessDBTask(
+ scoped_refptr<HistoryDBTaskRequest> request) {
+ DCHECK(request.get());
+ if (request->canceled())
+ return;
+
+ bool task_scheduled = !db_task_requests_.empty();
+ // Make sure we up the refcount of the request. ProcessDBTaskImpl will
+ // release when done with the task.
+ request->AddRef();
+ db_task_requests_.push_back(request.get());
+ if (!task_scheduled) {
+ // No other tasks are scheduled. Process request now.
+ ProcessDBTaskImpl();
+ }
+}
+
+void HistoryBackend::BroadcastNotifications(
+ NotificationType type,
+ HistoryDetails* details_deleted) {
+ DCHECK(delegate_.get());
+ delegate_->BroadcastNotifications(type, details_deleted);
+}
+
+// Deleting --------------------------------------------------------------------
+
+void HistoryBackend::DeleteAllHistory() {
+ // Our approach to deleting all history is:
+ // 1. Copy the bookmarks and their dependencies to new tables with temporary
+ // names.
+ // 2. Delete the original tables. Since tables can not share pages, we know
+ // that any data we don't want to keep is now in an unused page.
+ // 3. Renaming the temporary tables to match the original.
+ // 4. Vacuuming the database to delete the unused pages.
+ //
+ // Since we are likely to have very few bookmarks and their dependencies
+ // compared to all history, this is also much faster than just deleting from
+ // the original tables directly.
+ //
+ // TODO(brettw): bug 989802: When we store bookmarks in a separate file, this
+ // function can be simplified to having all the database objects close their
+ // connections and we just delete the files.
+
+ // Get starred entries and their corresponding URL rows.
+ std::vector<StarredEntry> starred_entries;
+ db_->GetStarredEntries(0, &starred_entries);
+
+ std::vector<URLRow> kept_urls;
+ for (size_t i = 0; i < starred_entries.size(); i++) {
+ if (starred_entries[i].type != StarredEntry::URL)
+ continue;
+
+ URLRow row;
+ if (!db_->GetURLRow(starred_entries[i].url_id, &row))
+ continue;
+
+ // Clear the last visit time so when we write these rows they are "clean."
+ // We keep the typed and visit counts. Since the kept URLs are bookmarks,
+ // we can assume that the user isn't trying to hide that they like them,
+ // and we can use these counts for giving better autocomplete suggestions.
+ row.set_last_visit(Time());
+
+ kept_urls.push_back(row);
+ }
+
+ // Clear thumbnail and favicon history. The favicons for the given URLs will
+ // be kept.
+ if (!ClearAllThumbnailHistory(&kept_urls)) {
+ LOG(ERROR) << "Thumbnail history could not be cleared";
+ // We continue in this error case. If the user wants to delete their
+ // history, we should delete as much as we can.
+ }
+
+ // ClearAllMainHistory will change the IDs of the URLs in kept_urls. Therfore,
+ // we clear the list afterwards to make sure nobody uses this invalid data.
+ if (!ClearAllMainHistory(&starred_entries, kept_urls))
+ LOG(ERROR) << "Main history could not be cleared";
+ kept_urls.clear();
+
+ // Delete FTS files & archived history.
+ if (text_database_.get()) {
+ // We assume that the text database has one transaction on them that we need
+ // to close & restart (the long-running history transaction).
+ text_database_->CommitTransaction();
+ text_database_->DeleteAll();
+ text_database_->BeginTransaction();
+ }
+
+ if (archived_db_.get()) {
+ // Close the database and delete the file.
+ archived_db_.reset();
+ std::wstring archived_file_name = GetArchivedFileName();
+ file_util::Delete(archived_file_name, false);
+
+ // Now re-initialize the database (which may fail).
+ archived_db_.reset(new ArchivedDatabase());
+ if (!archived_db_->Init(archived_file_name)) {
+ LOG(WARNING) << "Could not initialize the archived database.";
+ archived_db_.reset();
+ } else {
+ // Open our long-running transaction on this database.
+ archived_db_->BeginTransaction();
+ }
+ }
+
+ // Send out the notfication that history is cleared. The in-memory datdabase
+ // will pick this up and clear itself.
+ URLsDeletedDetails* details = new URLsDeletedDetails;
+ details->all_history = true;
+ BroadcastNotifications(NOTIFY_HISTORY_URLS_DELETED, details);
+}
+
+bool HistoryBackend::ClearAllThumbnailHistory(
+ std::vector<URLRow>* kept_urls) {
+ if (!thumbnail_db_.get()) {
+ // When we have no reference to the thumbnail database, maybe there was an
+ // error opening it. In this case, we just try to blow it away to try to
+ // fix the error if it exists. This may fail, in which case either the
+ // file doesn't exist or there's no more we can do.
+ file_util::Delete(GetThumbnailFileName(), false);
+ return true;
+ }
+
+ // Create the duplicate favicon table, this is where the favicons we want
+ // to keep will be stored.
+ if (!thumbnail_db_->InitTemporaryFavIconsTable())
+ return false;
+
+ // This maps existing favicon IDs to the ones in the temporary table.
+ typedef std::map<FavIconID, FavIconID> FavIconMap;
+ FavIconMap copied_favicons;
+
+ // Copy all unique favicons to the temporary table, and update all the
+ // URLs to have the new IDs.
+ for (std::vector<URLRow>::iterator i = kept_urls->begin();
+ i != kept_urls->end(); ++i) {
+ FavIconID old_id = i->favicon_id();
+ if (!old_id)
+ continue; // URL has no favicon.
+ FavIconID new_id;
+
+ FavIconMap::const_iterator found = copied_favicons.find(old_id);
+ if (found == copied_favicons.end()) {
+ new_id = thumbnail_db_->CopyToTemporaryFavIconTable(old_id);
+ copied_favicons[old_id] = new_id;
+ } else {
+ // We already encountered a URL that used this favicon, use the ID we
+ // previously got.
+ new_id = found->second;
+ }
+ i->set_favicon_id(new_id);
+ }
+
+ // Rename the duplicate favicon table back and recreate the other tables.
+ // This will make the database consistent again.
+ thumbnail_db_->CommitTemporaryFavIconTable();
+ thumbnail_db_->RecreateThumbnailTable();
+
+ // Vacuum to remove all the pages associated with the dropped tables. There
+ // must be no transaction open on the table when we do this. We assume that
+ // our long-running transaction is open, so we complete it and start it again.
+ DCHECK(thumbnail_db_->transaction_nesting() == 1);
+ thumbnail_db_->CommitTransaction();
+ thumbnail_db_->Vacuum();
+ thumbnail_db_->BeginTransaction();
+ return true;
+}
+
+bool HistoryBackend::ClearAllMainHistory(
+ std::vector<StarredEntry>* starred_entries,
+ const std::vector<URLRow>& kept_urls) {
+ // Create the duplicate URL table. We will copy the kept URLs into this.
+ if (!db_->CreateTemporaryURLTable())
+ return false;
+
+ // Insert the URLs into the temporary table, we need to keep a map of changed
+ // IDs since the ID will be different in the new table.
+ typedef std::map<URLID, URLID> URLIDMap;
+ URLIDMap old_to_new; // Maps original ID to new one.
+ for (std::vector<URLRow>::const_iterator i = kept_urls.begin();
+ i != kept_urls.end();
+ ++i) {
+ URLID new_id = db_->AddTemporaryURL(*i);
+ old_to_new[i->id()] = new_id;
+ }
+
+ // Replace the original URL table with the temporary one.
+ if (!db_->CommitTemporaryURLTable())
+ return false;
+
+ // The starred table references the old URL IDs. We need to fix it up to refer
+ // to the new ones.
+ for (std::vector<StarredEntry>::iterator i = starred_entries->begin();
+ i != starred_entries->end();
+ ++i) {
+ if (i->type != StarredEntry::URL)
+ continue;
+
+ DCHECK(old_to_new.find(i->url_id) != old_to_new.end());
+ i->url_id = old_to_new[i->url_id];
+ db_->UpdateURLIDForStar(i->id, i->url_id);
+ }
+
+ // Delete the old tables and recreate them empty.
+ db_->RecreateAllButStarAndURLTables();
+
+ // Vacuum to reclaim the space from the dropped tables. This must be done
+ // when there is no transaction open, and we assume that our long-running
+ // transaction is currently open.
+ db_->CommitTransaction();
+ db_->Vacuum();
+ db_->BeginTransaction();
+ return true;
+}
+
+} // namespace history
diff --git a/chrome/browser/history/history_backend.h b/chrome/browser/history/history_backend.h
new file mode 100644
index 0000000..c1136de
--- /dev/null
+++ b/chrome/browser/history/history_backend.h
@@ -0,0 +1,517 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_HISTORY_BACKEND_H__
+#define CHROME_BROWSER_HISTORY_HISTORY_BACKEND_H__
+
+#include <utility>
+
+#include "base/gfx/rect.h"
+#include "base/lock.h"
+#include "base/scoped_ptr.h"
+#include "base/task.h"
+#include "chrome/browser/history/archived_database.h"
+#include "chrome/browser/history/download_types.h"
+#include "chrome/browser/history/expire_history_backend.h"
+#include "chrome/browser/history/history_database.h"
+#include "chrome/browser/history/history_marshaling.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/history/page_usage_data.h"
+#include "chrome/browser/history/text_database_manager.h"
+#include "chrome/browser/history/thumbnail_database.h"
+#include "chrome/browser/history/visit_tracker.h"
+#include "chrome/common/mru_cache.h"
+#include "chrome/common/scoped_vector.h"
+#include "testing/gtest/include/gtest/gtest_prod.h"
+
+struct ThumbnailScore;
+
+namespace history {
+
+class CommitLaterTask;
+
+// *See the .cc file for more information on the design.*
+//
+// Internal history implementation which does most of the work of the history
+// system. This runs on a background thread (to not block the browser when we
+// do expensive operations) and is NOT threadsafe, so it must only be called
+// from message handlers on the background thread. Invoking on another thread
+// requires threadsafe refcounting.
+//
+// Most functions here are just the implementations of the corresponding
+// functions in the history service. These functions are not documented
+// here, see the history service for behavior.
+class HistoryBackend : public base::RefCountedThreadSafe<HistoryBackend>,
+ public BroadcastNotificationDelegate {
+ public:
+ // Interface implemented by the owner of the HistoryBackend object. Normally,
+ // the history service implements this to send stuff back to the main thread.
+ // The unit tests can provide a different implementation if they don't have
+ // a history service object.
+ class Delegate {
+ public:
+ virtual ~Delegate() {}
+
+ // Called when the database is from a future version of the product and can
+ // not be used.
+ virtual void NotifyTooNew() = 0;
+
+ // Sets the in-memory history backend. The in-memory backend is created by
+ // the main backend. For non-unit tests, this happens on the background
+ // thread. It is to be used on the main thread, so this would transfer
+ // it to the history service. Unit tests can override this behavior.
+ //
+ // This function is NOT guaranteed to be called. If there is an error,
+ // there may be no in-memory database.
+ //
+ // Ownership of the backend pointer is transferred to this function.
+ virtual void SetInMemoryBackend(InMemoryHistoryBackend* backend) = 0;
+
+ // Broadcasts the specified notification to the notification service.
+ // This is implemented here because notifications must only be sent from
+ // the main thread.
+ //
+ // Ownership of the HistoryDetails is transferred to this function.
+ virtual void BroadcastNotifications(NotificationType type,
+ HistoryDetails* details) = 0;
+ };
+
+ // Init must be called to complete object creation. This object can be
+ // constructed on any thread, but all other functions including Init() must
+ // be called on the history thread.
+ //
+ // |history_dir| is the directory where the history files will be placed.
+ // See the definition of BroadcastNotificationsCallback above. This function
+ // takes ownership of the callback pointer.
+ //
+ // This constructor is fast and does no I/O, so can be called at any time.
+ HistoryBackend(const std::wstring& history_dir, Delegate* delegate);
+
+ ~HistoryBackend();
+
+ // Must be called after creation but before any objects are created. If this
+ // fails, all other functions will fail as well. (Since this runs on another
+ // thread, we don't bother returning failure.)
+ void Init();
+
+ // Notification that the history system is shutting down. This will break
+ // the refs owned by the delegate and any pending transaction so it will
+ // actually be deleted.
+ void Closing();
+
+ // See NotifyRenderProcessHostDestruction.
+ void NotifyRenderProcessHostDestruction(const void* host);
+
+ // Navigation ----------------------------------------------------------------
+
+ void AddPage(scoped_refptr<HistoryAddPageArgs> request);
+ void SetPageTitle(const GURL& url, const std::wstring& title);
+ void AddPageWithDetails(const URLRow& info);
+
+ // Indexing ------------------------------------------------------------------
+
+ void SetPageContents(const GURL& url, const std::wstring& contents);
+
+ // Querying ------------------------------------------------------------------
+
+ // ScheduleAutocomplete() never frees |provider| (which is globally live).
+ // It passes |params| on to the autocomplete system which will eventually
+ // free it.
+ void ScheduleAutocomplete(HistoryURLProvider* provider,
+ HistoryURLProviderParams* params);
+
+ void IterateURLs(HistoryService::URLEnumerator* enumerator);
+ void QueryURL(scoped_refptr<QueryURLRequest> request,
+ const GURL& url,
+ bool want_visits);
+ void QueryHistory(scoped_refptr<QueryHistoryRequest> request,
+ const std::wstring& text_query,
+ const QueryOptions& options);
+ void QueryRedirectsFrom(scoped_refptr<QueryRedirectsRequest> request,
+ const GURL& url);
+
+ void GetVisitCountToHost(scoped_refptr<GetVisitCountToHostRequest> request,
+ const GURL& url);
+
+ // Computes the most recent URL(s) that the given canonical URL has
+ // redirected to and returns true on success. There may be more than one
+ // redirect in a row, so this function will fill the given array with the
+ // entire chain. If there are no redirects for the most recent visit of the
+ // URL, or the URL is not in history, returns false.
+ //
+ // Backend for QueryRedirectsFrom.
+ bool GetMostRecentRedirectsFrom(const GURL& url,
+ HistoryService::RedirectList* redirects);
+
+ // Thumbnails ----------------------------------------------------------------
+
+ void SetPageThumbnail(const GURL& url,
+ const SkBitmap& thumbnail,
+ const ThumbnailScore& score);
+
+ // Retrieves a thumbnail, passing it across thread boundaries
+ // via. the included callback.
+ void GetPageThumbnail(scoped_refptr<GetPageThumbnailRequest> request,
+ const GURL& page_url);
+
+ // Backend implementation of GetPageThumbnail. Unlike
+ // GetPageThumbnail(), this method has way to transport data across
+ // thread boundaries.
+ //
+ // Exposed for testing reasons.
+ void GetPageThumbnailDirectly(
+ const GURL& page_url,
+ scoped_refptr<RefCountedBytes>* data);
+
+ // Favicon -------------------------------------------------------------------
+
+ void GetFavIcon(scoped_refptr<GetFavIconRequest> request,
+ const GURL& icon_url);
+ void GetFavIconForURL(scoped_refptr<GetFavIconRequest> request,
+ const GURL& page_url);
+ void SetFavIcon(const GURL& page_url,
+ const GURL& icon_url,
+ scoped_refptr<RefCountedBytes> data);
+ void UpdateFavIconMappingAndFetch(scoped_refptr<GetFavIconRequest> request,
+ const GURL& page_url,
+ const GURL& icon_url);
+ void SetFavIconOutOfDateForPage(const GURL& page_url);
+ void SetImportedFavicons(
+ const std::vector<ImportedFavIconUsage>& favicon_usage);
+
+ // Starring ------------------------------------------------------------------
+
+ void GetAllStarredEntries(
+ scoped_refptr<GetStarredEntriesRequest> request);
+
+ void UpdateStarredEntry(const StarredEntry& new_entry);
+
+ void CreateStarredEntry(scoped_refptr<CreateStarredEntryRequest> request,
+ const StarredEntry& entry);
+
+ void DeleteStarredGroup(UIStarID group_id);
+
+ void DeleteStarredURL(const GURL& url);
+
+ void DeleteStarredEntry(history::StarID star_id);
+
+ void GetMostRecentStarredEntries(
+ scoped_refptr<GetMostRecentStarredEntriesRequest> request,
+ int max_count);
+
+ // Downloads -----------------------------------------------------------------
+
+ void QueryDownloads(scoped_refptr<DownloadQueryRequest> request);
+ void UpdateDownload(int64 received_bytes, int32 state, int64 db_handle);
+ void CreateDownload(scoped_refptr<DownloadCreateRequest> request,
+ const DownloadCreateInfo& info);
+ void RemoveDownload(int64 db_handle);
+ void RemoveDownloadsBetween(const Time remove_begin,
+ const Time remove_end);
+ void RemoveDownloads(const Time remove_end);
+ void SearchDownloads(scoped_refptr<DownloadSearchRequest>,
+ const std::wstring& search_text);
+
+ // Segment usage -------------------------------------------------------------
+
+ void QuerySegmentUsage(scoped_refptr<QuerySegmentUsageRequest> request,
+ const Time from_time);
+ void DeleteOldSegmentData();
+ void SetSegmentPresentationIndex(SegmentID segment_id, int index);
+
+ // Keyword search terms ------------------------------------------------------
+
+ void SetKeywordSearchTermsForURL(const GURL& url,
+ TemplateURL::IDType keyword_id,
+ const std::wstring& term);
+
+ void DeleteAllSearchTermsForKeyword(TemplateURL::IDType keyword_id);
+
+ void GetMostRecentKeywordSearchTerms(
+ scoped_refptr<GetMostRecentKeywordSearchTermsRequest> request,
+ TemplateURL::IDType keyword_id,
+ const std::wstring& prefix,
+ int max_count);
+
+ // Generic operations --------------------------------------------------------
+
+ void ProcessDBTask(scoped_refptr<HistoryDBTaskRequest> request);
+
+ // Deleting ------------------------------------------------------------------
+
+ void DeleteURL(const GURL& url);
+
+ // Calls ExpireHistoryBackend::ExpireHistoryBetween and commits the change.
+ void ExpireHistoryBetween(scoped_refptr<ExpireHistoryRequest> request,
+ Time begin_time,
+ Time end_time);
+
+ // Testing -------------------------------------------------------------------
+
+ // Sets the task to run and the message loop to run it on when this object
+ // is destroyed. See HistoryService::SetOnBackendDestroyTask for a more
+ // complete description.
+ void SetOnBackendDestroyTask(MessageLoop* message_loop, Task* task);
+
+ // Adds the given rows to the database if it doesn't exist. A visit will be
+ // added for each given URL at the last visit time in the URLRow.
+ void AddPagesWithDetails(const std::vector<URLRow>& info);
+
+ private:
+ friend class CommitLaterTask; // The commit task needs to call Commit().
+ friend class HistoryTest; // So the unit tests can poke our innards.
+ FRIEND_TEST(HistoryBackendTest, DeleteAll);
+
+ // For invoking methods that circumvent requests.
+ friend class HistoryTest;
+
+ // Computes the name of the specified database on disk.
+ std::wstring GetThumbnailFileName() const;
+ std::wstring GetArchivedFileName() const;
+
+ class URLQuerier;
+ friend class URLQuerier;
+
+ // Adds a single visit to the database, updating the URL information such
+ // as visit and typed count. The visit ID of the added visit and the URL ID
+ // of the associated URL (whether added or not) is returned. Both values will
+ // be 0 on failure.
+ //
+ // This does not schedule database commits, it is intended to be used as a
+ // subroutine for AddPage only. It also assumes the database is valid.
+ std::pair<URLID, VisitID> AddPageVisit(const GURL& url,
+ Time time,
+ VisitID referring_visit,
+ PageTransition::Type transition);
+
+ // Returns a redirect chain in |redirects| for the VisitID
+ // |cur_visit|. |cur_visit| is assumed to be valid. Assumes that
+ // this HistoryBackend object has been Init()ed successfully.
+ void GetRedirectsFromSpecificVisit(
+ VisitID cur_visit, HistoryService::RedirectList* redirects);
+
+ // Thumbnail Helpers ---------------------------------------------------------
+
+ // When a simple GetMostRecentRedirectsFrom() fails, this method is
+ // called which searches the last N visit sessions instead of just
+ // the current one. Returns true and puts thumbnail data in |data|
+ // if a proper thumbnail was found. Returns false otherwise. Assumes
+ // that this HistoryBackend object has been Init()ed successfully.
+ bool GetThumbnailFromOlderRedirect(
+ const GURL& page_url, std::vector<unsigned char>* data);
+
+ // Querying ------------------------------------------------------------------
+
+ // Backends for QueryHistory. *Basic() handles queries that are not FTS (full
+ // text search) queries and can just be given directly to the history DB).
+ // The FTS version queries the text_database, then merges with the history DB.
+ // Both functions assume QueryHistory already checked the DB for validity.
+ void QueryHistoryBasic(URLDatabase* url_db, VisitDatabase* visit_db,
+ const QueryOptions& options, QueryResults* result);
+ void QueryHistoryFTS(const std::wstring& text_query,
+ const QueryOptions& options,
+ QueryResults* result);
+
+ // Queries the starred database for all URL entries whose title contains the
+ // specified text. This is called as necessary from QueryHistoryFTS. The
+ // matches will be added to the beginning of the result vector in no
+ // particular order.
+ void QueryStarredEntriesByText(URLQuerier* querier,
+ const std::wstring& text_query,
+ const QueryOptions& options,
+ QueryResults* results);
+
+ // Committing ----------------------------------------------------------------
+
+ // We always keep a transaction open on the history database so that multiple
+ // transactions can be batched. Periodically, these are flushed (use
+ // ScheduleCommit). This function does the commit to write any new changes to
+ // disk and opens a new transaction. This will be called automatically by
+ // ScheduleCommit, or it can be called explicitly if a caller really wants
+ // to write something to disk.
+ void Commit();
+
+ // Schedules a commit to happen in the future. We do this so that many
+ // operations over a period of time will be batched together. If there is
+ // already a commit scheduled for the future, this will do nothing.
+ void ScheduleCommit();
+
+ // Cancels the scheduled commit, if any. If there is no scheduled commit,
+ // does nothing.
+ void CancelScheduledCommit();
+
+ // Segments ------------------------------------------------------------------
+
+ // Walks back a segment chain to find the last visit with a non null segment
+ // id and returns it. If there is none found, returns 0.
+ SegmentID GetLastSegmentID(VisitID from_visit);
+
+ // Update the segment information. This is called internally when a page is
+ // added. Return the segment id of the segment that has been updated.
+ SegmentID UpdateSegments(const GURL& url,
+ VisitID from_visit,
+ VisitID visit_id,
+ PageTransition::Type transition_type,
+ const Time ts);
+
+ // Favicons ------------------------------------------------------------------
+
+ // Used by both UpdateFavIconMappingAndFetch and GetFavIcon.
+ // If page_url is non-null and SetFavIcon has previously been invoked for
+ // icon_url the favicon url for page_url (and all redirects) is set to
+ // icon_url.
+ void UpdateFavIconMappingAndFetchImpl(
+ const GURL* page_url,
+ const GURL& icon_url,
+ scoped_refptr<GetFavIconRequest> request);
+
+ // Sets the favicon url id for page_url to id. This will also broadcast
+ // notifications as necessary.
+ void SetFavIconMapping(const GURL& page_url, FavIconID id);
+
+ // Generic stuff -------------------------------------------------------------
+
+ // Processes the next scheduled HistoryDBTask, scheduling this method
+ // to be invoked again if there are more tasks that need to run.
+ void ProcessDBTaskImpl();
+
+ // Release all tasks in history_db_tasks_ and clears it.
+ void ReleaseDBTasks();
+
+ // Schedules a broadcast of the given notification on the main thread. The
+ // details argument will have ownership taken by this function (it will be
+ // sent to the main thread and deleted there).
+ void BroadcastNotifications(NotificationType type,
+ HistoryDetails* details_deleted);
+
+ // Deleting all history ------------------------------------------------------
+
+ // Deletes all history. This is a special case of deleting that is separated
+ // from our normal dependency-following method for performance reasons. The
+ // logic lives here instead of ExpireHistoryBackend since it will cause
+ // re-initialization of some databases such as Thumbnails or Archived that
+ // could fail. When these databases are not valid, our pointers must be NULL,
+ // so we need to handle this type of operation to keep the pointers in sync.
+ void DeleteAllHistory();
+
+ // Given a vector of all URLs that we will keep, removes all thumbnails
+ // referenced by any URL, and also all favicons that aren't used by those
+ // URLs. The favicon IDs will change, so this will update the url rows in the
+ // vector to reference the new IDs.
+ bool ClearAllThumbnailHistory(std::vector<URLRow>* kept_urls);
+
+ // Deletes all information in the history database, except the star table
+ // (all entries should be in the given vector) and the given URLs in the URL
+ // table (these should correspond to the bookmarked URLs).
+ //
+ // The IDs of the URLs may change, and the starred table will be updated
+ // accordingly. This function will also update the |starred_entries| input
+ // vector.
+ bool ClearAllMainHistory(std::vector<StarredEntry>* starred_entries,
+ const std::vector<URLRow>& kept_urls);
+
+ // Data ----------------------------------------------------------------------
+
+ // Delegate. See the class definition above for more information. This will
+ // be NULL before Init is called and after Cleanup, but is guaranteed
+ // non-NULL in between.
+ scoped_ptr<Delegate> delegate_;
+
+ // Directory where database files will be stored.
+ std::wstring history_dir_;
+
+ // The history/thumbnail databases. Either MAY BE NULL if the database could
+ // not be opened, all users must first check for NULL and return immediately
+ // if it is. The thumbnail DB may be NULL when the history one isn't, but not
+ // vice-versa.
+ scoped_ptr<HistoryDatabase> db_;
+ scoped_ptr<ThumbnailDatabase> thumbnail_db_;
+
+ // Stores old history in a larger, slower database.
+ scoped_ptr<ArchivedDatabase> archived_db_;
+
+ // Full text database manager, possibly NULL if the database could not be
+ // created.
+ scoped_ptr<TextDatabaseManager> text_database_;
+
+ // Manages expiration between the various databases.
+ ExpireHistoryBackend expirer_;
+
+ // A commit has been scheduled to occur sometime in the future. We can check
+ // non-null-ness to see if there is a commit scheduled in the future, and we
+ // can use the pointer to cancel the scheduled commit. There can be only one
+ // scheduled commit at a time (see ScheduleCommit).
+ scoped_refptr<CommitLaterTask> scheduled_commit_;
+
+ // Maps recent redirect destination pages to the chain of redirects that
+ // brought us to there. Pages that did not have redirects or were not the
+ // final redirect in a chain will not be in this list, as well as pages that
+ // redirected "too long" ago (as determined by ExpireOldRedirects above).
+ // It is used to set titles & favicons for redirects to that of the
+ // destination.
+ //
+ // As with AddPage, the last item in the redirect chain will be the
+ // destination of the redirect (i.e., the key into recent_redirects_);
+ typedef MRUCache<GURL, HistoryService::RedirectList> RedirectCache;
+ RedirectCache recent_redirects_;
+
+ // Timestamp of the last page addition request. We use this to detect when
+ // multiple additions are requested at the same time (within the resolution
+ // of the timer), so we can try to ensure they're unique when they're added
+ // to the database by using the last_recorded_time_ (q.v.). We still can't
+ // enforce or guarantee uniqueness, since the user might set his clock back.
+ Time last_requested_time_;
+
+ // Timestamp of the last page addition, as it was recorded in the database.
+ // If two or more requests come in at the same time, we increment that time
+ // by 1 us between them so it's more likely to be unique in the database.
+ // This keeps track of that higher-resolution timestamp.
+ Time last_recorded_time_;
+
+ // When non-NULL, this is the task that should be invoked on
+ MessageLoop* backend_destroy_message_loop_;
+ Task* backend_destroy_task_;
+
+ // Tracks page transition types.
+ VisitTracker tracker_;
+
+ // A boolean variable to track whether we have already purged obsolete segment
+ // data.
+ bool segment_queried_;
+
+ // HistoryDBTasks to run. Be sure to AddRef when adding, and Release when
+ // done.
+ std::list<HistoryDBTaskRequest*> db_task_requests_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(HistoryBackend);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_HISTORY_BACKEND_H__
diff --git a/chrome/browser/history/history_backend_unittest.cc b/chrome/browser/history/history_backend_unittest.cc
new file mode 100644
index 0000000..e1e6ffe
--- /dev/null
+++ b/chrome/browser/history/history_backend_unittest.cc
@@ -0,0 +1,316 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/scoped_ptr.h"
+#include "chrome/browser/history/history_backend.h"
+#include "chrome/browser/history/in_memory_history_backend.h"
+#include "chrome/browser/history/in_memory_database.h"
+#include "chrome/common/jpeg_codec.h"
+#include "chrome/common/thumbnail_score.h"
+#include "chrome/tools/profiles/thumbnail-inl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+// This file only tests functionality where it is most convenient to call the
+// backend directly. Most of the history backend functions are tested by the
+// history unit test. Because of the elaborate callbacks involved, this is no
+// harder than calling it directly for many things.
+
+namespace history {
+
+class HistoryBackendTest;
+
+// This must be a separate object since HistoryBackend manages its lifetime.
+// This just forwards the messages we're interested in to the test object.
+class HistoryBackendTestDelegate : public HistoryBackend::Delegate {
+ public:
+ explicit HistoryBackendTestDelegate(HistoryBackendTest* test) : test_(test) {
+ }
+
+ virtual void NotifyTooNew() {
+ }
+ virtual void SetInMemoryBackend(InMemoryHistoryBackend* backend);
+ virtual void BroadcastNotifications(NotificationType type,
+ HistoryDetails* details);
+
+ private:
+ // Not owned by us.
+ HistoryBackendTest* test_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(HistoryBackendTestDelegate);
+};
+
+class HistoryBackendTest : public testing::Test {
+ public:
+ virtual ~HistoryBackendTest() {
+ }
+
+ protected:
+ scoped_refptr<HistoryBackend> backend_; // Will be NULL on init failure.
+ scoped_ptr<InMemoryHistoryBackend> mem_backend_;
+
+ void AddRedirectChain(const wchar_t* sequence[], int page_id) {
+ HistoryService::RedirectList redirects;
+ for (int i = 0; sequence[i] != NULL; ++i)
+ redirects.push_back(GURL(sequence[i]));
+
+ int int_scope = 1;
+ void* scope = 0;
+ memcpy(&scope, &int_scope, sizeof(int_scope));
+ scoped_refptr<history::HistoryAddPageArgs> request(
+ new history::HistoryAddPageArgs(
+ redirects.back(), Time::Now(), scope, page_id, GURL(),
+ redirects, PageTransition::LINK));
+ backend_->AddPage(request);
+ }
+
+ private:
+ friend HistoryBackendTestDelegate;
+
+ // testing::Test
+ virtual void SetUp() {
+ if (!file_util::CreateNewTempDirectory(L"BackendTest", &test_dir_))
+ return;
+ backend_ = new HistoryBackend(test_dir_,
+ new HistoryBackendTestDelegate(this));
+ backend_->Init();
+ }
+ virtual void TearDown() {
+ backend_->Closing();
+ backend_ = NULL;
+ mem_backend_.reset();
+ file_util::Delete(test_dir_, true);
+ }
+
+ void SetInMemoryBackend(InMemoryHistoryBackend* backend) {
+ mem_backend_.reset(backend);
+ }
+
+ void BroadcastNotifications(NotificationType type,
+ HistoryDetails* details) {
+ // Send the notifications directly to the in-memory database.
+ Details<HistoryDetails> det(details);
+ mem_backend_->Observe(type, Source<HistoryTest>(NULL), det);
+
+ // The backend passes ownership of the details pointer to us.
+ delete details;
+ }
+
+ std::wstring test_dir_;
+};
+
+void HistoryBackendTestDelegate::SetInMemoryBackend(
+ InMemoryHistoryBackend* backend) {
+ test_->SetInMemoryBackend(backend);
+}
+
+void HistoryBackendTestDelegate::BroadcastNotifications(
+ NotificationType type,
+ HistoryDetails* details) {
+ test_->BroadcastNotifications(type, details);
+}
+
+
+TEST_F(HistoryBackendTest, DeleteAll) {
+ ASSERT_TRUE(backend_.get());
+
+ // Add two favicons, use the characters '1' and '2' for the image data. Note
+ // that we do these in the opposite order. This is so the first one gets ID
+ // 2 autoassigned to the database, which will change when the other one is
+ // deleted. This way we can test that updating works properly.
+ GURL favicon_url1("http://www.google.com/favicon.ico");
+ GURL favicon_url2("http://news.google.com/favicon.ico");
+ FavIconID favicon2 = backend_->thumbnail_db_->AddFavIcon(favicon_url2);
+ FavIconID favicon1 = backend_->thumbnail_db_->AddFavIcon(favicon_url1);
+
+ std::vector<unsigned char> data;
+ data.push_back('1');
+ EXPECT_TRUE(backend_->thumbnail_db_->SetFavIcon(
+ favicon1, data, Time::Now()));
+
+ data[0] = '2';
+ EXPECT_TRUE(backend_->thumbnail_db_->SetFavIcon(
+ favicon2, data, Time::Now()));
+
+ // First visit two URLs.
+ URLRow row1(GURL("http://www.google.com/"));
+ row1.set_visit_count(2);
+ row1.set_typed_count(1);
+ row1.set_last_visit(Time::Now());
+ row1.set_favicon_id(favicon1);
+
+ URLRow row2(GURL("http://news.google.com/"));
+ row2.set_visit_count(1);
+ row2.set_last_visit(Time::Now());
+ row2.set_favicon_id(favicon2);
+
+ std::vector<URLRow> rows;
+ rows.push_back(row2); // Reversed order for the same reason as favicons.
+ rows.push_back(row1);
+ backend_->AddPagesWithDetails(rows);
+
+ URLID row1_id = backend_->db_->GetRowForURL(row1.url(), NULL);
+ URLID row2_id = backend_->db_->GetRowForURL(row2.url(), NULL);
+
+ // Get the two visits for the URLs we just added.
+ VisitVector visits;
+ backend_->db_->GetVisitsForURL(row1_id, &visits);
+ ASSERT_EQ(1, visits.size());
+ VisitID visit1_id = visits[0].visit_id;
+
+ visits.clear();
+ backend_->db_->GetVisitsForURL(row2_id, &visits);
+ ASSERT_EQ(1, visits.size());
+ VisitID visit2_id = visits[0].visit_id;
+
+ // The in-memory backend should have been set and it should have gotten the
+ // typed URL.
+ ASSERT_TRUE(mem_backend_.get());
+ URLRow outrow1;
+ EXPECT_TRUE(mem_backend_->db_->GetRowForURL(row1.url(), NULL));
+
+ // Add thumbnails for each page.
+ ThumbnailScore score(0.25, true, true);
+ scoped_ptr<SkBitmap> google_bitmap(
+ JPEGCodec::Decode(kGoogleThumbnail, sizeof(kGoogleThumbnail)));
+ backend_->thumbnail_db_->SetPageThumbnail(row1_id, *google_bitmap, score);
+ scoped_ptr<SkBitmap> weewar_bitmap(
+ JPEGCodec::Decode(kWeewarThumbnail, sizeof(kWeewarThumbnail)));
+ backend_->thumbnail_db_->SetPageThumbnail(row2_id, *weewar_bitmap, score);
+
+ // Mark one of the URLs bookmarked.
+ StarredEntry entry;
+ entry.type = StarredEntry::URL;
+ entry.url = row1.url();
+ entry.parent_group_id = HistoryService::kBookmarkBarID;
+ StarID star_id = backend_->db_->CreateStarredEntry(&entry);
+ ASSERT_TRUE(star_id);
+
+ // Set full text index for each one.
+ backend_->text_database_->AddPageData(row1.url(), row1_id, visit1_id,
+ row1.last_visit(),
+ L"Title 1", L"Body 1");
+ backend_->text_database_->AddPageData(row2.url(), row2_id, visit2_id,
+ row2.last_visit(),
+ L"Title 2", L"Body 2");
+
+ // Now finally clear all history.
+ backend_->DeleteAllHistory();
+
+ // The first URL should be preserved, along with its visits, but the time
+ // should be cleared.
+ EXPECT_TRUE(backend_->db_->GetRowForURL(row1.url(), &outrow1));
+ EXPECT_EQ(2, outrow1.visit_count());
+ EXPECT_EQ(1, outrow1.typed_count());
+ EXPECT_TRUE(Time() == outrow1.last_visit());
+
+ // The second row should be deleted.
+ URLRow outrow2;
+ EXPECT_FALSE(backend_->db_->GetRowForURL(row2.url(), &outrow2));
+
+ // All visits should be deleted for both URLs.
+ VisitVector all_visits;
+ backend_->db_->GetAllVisitsInRange(Time(), Time(), 0, &all_visits);
+ ASSERT_EQ(0, all_visits.size());
+
+ // All thumbnails should be deleted.
+ std::vector<unsigned char> out_data;
+ EXPECT_FALSE(backend_->thumbnail_db_->GetPageThumbnail(outrow1.id(),
+ &out_data));
+ EXPECT_FALSE(backend_->thumbnail_db_->GetPageThumbnail(row2_id, &out_data));
+
+ // We should have a favicon for the first URL only. We look them up by favicon
+ // URL since the IDs may hav changed.
+ FavIconID out_favicon1 = backend_->thumbnail_db_->
+ GetFavIconIDForFavIconURL(favicon_url1);
+ EXPECT_TRUE(out_favicon1);
+ FavIconID out_favicon2 = backend_->thumbnail_db_->
+ GetFavIconIDForFavIconURL(favicon_url2);
+ EXPECT_FALSE(out_favicon2) << "Favicon not deleted";
+
+ // The remaining URL should still reference the same favicon, even if its
+ // ID has changed.
+ EXPECT_EQ(out_favicon1, outrow1.favicon_id());
+
+ // The first URL should still be bookmarked and have an entry. The star ID
+ // must not have changed.
+ EXPECT_TRUE(outrow1.starred());
+ StarredEntry outentry;
+ EXPECT_TRUE(backend_->db_->GetStarredEntry(star_id, &outentry));
+ EXPECT_EQ(outrow1.id(), outentry.url_id);
+ EXPECT_TRUE(outrow1.url() == outentry.url);
+
+ // The full text database should have no data.
+ std::vector<TextDatabase::Match> text_matches;
+ Time first_time_searched;
+ backend_->text_database_->GetTextMatches(L"Body", QueryOptions(),
+ &text_matches,
+ &first_time_searched);
+ EXPECT_EQ(0, text_matches.size());
+}
+
+TEST_F(HistoryBackendTest, GetPageThumbnailAfterRedirects) {
+ ASSERT_TRUE(backend_.get());
+
+ const wchar_t* base_url = L"http://mail";
+ const wchar_t* thumbnail_url = L"http://mail.google.com";
+ const wchar_t* first_chain[] = {
+ base_url,
+ thumbnail_url,
+ NULL
+ };
+ AddRedirectChain(first_chain, 0);
+
+ // Add a thumbnail for the end of that redirect chain.
+ scoped_ptr<SkBitmap> thumbnail(
+ JPEGCodec::Decode(kGoogleThumbnail, sizeof(kGoogleThumbnail)));
+ backend_->SetPageThumbnail(GURL(thumbnail_url), *thumbnail,
+ ThumbnailScore(0.25, true, true));
+
+ // Write a second URL chain so that if you were to simply check what
+ // "http://mail" redirects to, you wouldn't see the URL that has
+ // contains the thumbnail.
+ const wchar_t* second_chain[] = {
+ base_url,
+ L"http://mail.google.com/somewhere/else",
+ NULL
+ };
+ AddRedirectChain(second_chain, 1);
+
+ // Now try to get the thumbnail for the base url. It shouldn't be
+ // distracted by the second chain and should return the thumbnail
+ // attached to thumbnail_url_.
+ scoped_refptr<RefCountedBytes> data;
+ backend_->GetPageThumbnailDirectly(GURL(base_url), &data);
+
+ EXPECT_TRUE(data.get());
+}
+
+} // namespace history
diff --git a/chrome/browser/history/history_database.cc b/chrome/browser/history/history_database.cc
new file mode 100644
index 0000000..e606aeb
--- /dev/null
+++ b/chrome/browser/history/history_database.cc
@@ -0,0 +1,248 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/history_database.h"
+
+#include <algorithm>
+#include <set>
+
+#include "base/string_util.h"
+
+// Only needed for migration.
+#include "base/file_util.h"
+#include "chrome/browser/history/text_database_manager.h"
+
+namespace history {
+
+namespace {
+
+// Current version number.
+const int kCurrentVersionNumber = 15;
+
+} // namespace
+
+HistoryDatabase::HistoryDatabase()
+ : transaction_nesting_(0),
+ db_(NULL) {
+}
+
+HistoryDatabase::~HistoryDatabase() {
+}
+
+InitStatus HistoryDatabase::Init(const std::wstring& history_name) {
+ // Open the history database, using the narrow version of open indicates to
+ // sqlite that we want the database to be in UTF-8 if it doesn't already
+ // exist.
+ DCHECK(!db_) << "Already initialized!";
+ if (sqlite3_open(WideToUTF8(history_name).c_str(), &db_) != SQLITE_OK)
+ return INIT_FAILURE;
+ statement_cache_ = new SqliteStatementCache;
+ DBCloseScoper scoper(&db_, &statement_cache_);
+
+ // Set the database page size to something a little larger to give us
+ // better performance (we're typically seek rather than bandwidth limited).
+ // This only has an effect before any tables have been created, otherwise
+ // this is a NOP. Must be a power of 2 and a max of 8192.
+ sqlite3_exec(db_, "PRAGMA page_size=4096", NULL, NULL, NULL);
+
+ // Increase the cache size. The page size, plus a little extra, times this
+ // value, tells us how much memory the cache will use maximum.
+ // 6000 * 4MB = 24MB
+ // TODO(brettw) scale this value to the amount of available memory.
+ sqlite3_exec(db_, "PRAGMA cache_size=6000", NULL, NULL, NULL);
+
+ // Wrap the rest of init in a tranaction. This will prevent the database from
+ // getting corrupted if we crash in the middle of initialization or migration.
+ TransactionScoper transaction(this);
+
+ // Make sure the statement cache is properly initialized.
+ statement_cache_->set_db(db_);
+
+ // Prime the cache. See the header file's documentation for this function.
+ PrimeCache();
+
+ // Create the tables and indices.
+ // NOTE: If you add something here, also add it to
+ // RecreateAllButStarAndURLTables.
+ if (!meta_table_.Init(std::string(), kCurrentVersionNumber, db_))
+ return INIT_FAILURE;
+ if (!CreateURLTable(false) || !InitVisitTable() ||
+ !InitKeywordSearchTermsTable() || !InitDownloadTable() ||
+ !InitSegmentTables() || !InitStarTable())
+ return INIT_FAILURE;
+ CreateMainURLIndex();
+ CreateSupplimentaryURLIndices();
+
+ // Version check.
+ InitStatus version_status = EnsureCurrentVersion();
+ if (version_status != INIT_OK)
+ return version_status;
+
+ EnsureStarredIntegrity();
+
+ // Succeeded: keep the DB open by detaching the auto-closer.
+ scoper.Detach();
+ db_closer_.Attach(&db_, &statement_cache_);
+ return INIT_OK;
+}
+
+void HistoryDatabase::BeginExclusiveMode() {
+ sqlite3_exec(db_, "PRAGMA locking_mode=EXCLUSIVE", NULL, NULL, NULL);
+}
+
+void HistoryDatabase::PrimeCache() {
+ // A statement must be open for the preload command to work. If the meta
+ // table can't be read, it probably means this is a new database and there
+ // is nothing to preload (so it's OK we do nothing).
+ SQLStatement dummy;
+ if (dummy.prepare(db_, "SELECT * from meta") != SQLITE_OK)
+ return;
+ if (dummy.step() != SQLITE_ROW)
+ return;
+
+ sqlite3Preload(db_);
+}
+
+// static
+int HistoryDatabase::GetCurrentVersion() {
+ return kCurrentVersionNumber;
+}
+
+void HistoryDatabase::BeginTransaction() {
+ DCHECK(db_);
+ if (transaction_nesting_ == 0) {
+ int rv = sqlite3_exec(db_, "BEGIN TRANSACTION", NULL, NULL, NULL);
+ DCHECK(rv == SQLITE_OK) << "Failed to begin transaction";
+ }
+ transaction_nesting_++;
+}
+
+void HistoryDatabase::CommitTransaction() {
+ DCHECK(db_);
+ DCHECK(transaction_nesting_ > 0) << "Committing too many transactions";
+ transaction_nesting_--;
+ if (transaction_nesting_ == 0) {
+ int rv = sqlite3_exec(db_, "COMMIT", NULL, NULL, NULL);
+ DCHECK(rv == SQLITE_OK) << "Failed to commit transaction";
+ }
+}
+
+bool HistoryDatabase::RecreateAllButStarAndURLTables() {
+ if (!DropVisitTable())
+ return false;
+ if (!InitVisitTable())
+ return false;
+
+ if (!DropKeywordSearchTermsTable())
+ return false;
+ if (!InitKeywordSearchTermsTable())
+ return false;
+
+ if (!DropSegmentTables())
+ return false;
+ if (!InitSegmentTables())
+ return false;
+
+ // We also add the supplimentary URL indices at this point. This index is
+ // over parts of the URL table that weren't automatically created when the
+ // temporary URL table was
+ CreateSupplimentaryURLIndices();
+ return true;
+}
+
+void HistoryDatabase::Vacuum() {
+ DCHECK(transaction_nesting_ == 0) <<
+ "Can not have a transaction when vacuuming.";
+ sqlite3_exec(db_, "VACUUM", NULL, NULL, NULL);
+}
+
+bool HistoryDatabase::SetSegmentID(VisitID visit_id, SegmentID segment_id) {
+ SQLStatement s;
+ if (s.prepare(db_, "UPDATE visits SET segment_id = ? WHERE id = ?") !=
+ SQLITE_OK) {
+ NOTREACHED();
+ return false;
+ }
+ s.bind_int64(0, segment_id);
+ s.bind_int64(1, visit_id);
+ return s.step() == SQLITE_DONE;
+}
+
+SegmentID HistoryDatabase::GetSegmentID(VisitID visit_id) {
+ SQLStatement s;
+ if (s.prepare(db_, "SELECT segment_id FROM visits WHERE id = ?")
+ != SQLITE_OK) {
+ NOTREACHED();
+ return 0;
+ }
+
+ s.bind_int64(0, visit_id);
+ if (s.step() == SQLITE_ROW) {
+ if (s.column_type(0) == SQLITE_NULL)
+ return 0;
+ else
+ return s.column_int64(0);
+ }
+ return 0;
+}
+
+sqlite3* HistoryDatabase::GetDB() {
+ return db_;
+}
+
+SqliteStatementCache& HistoryDatabase::GetStatementCache() {
+ return *statement_cache_;
+}
+
+// Migration -------------------------------------------------------------------
+
+InitStatus HistoryDatabase::EnsureCurrentVersion() {
+ // We can't read databases newer than we were designed for.
+ if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber)
+ return INIT_TOO_NEW;
+
+ // NOTICE: If you are changing structures for things shared with the archived
+ // history file like URLs, visits, or downloads, that will need migration as
+ // well. Instead of putting such migration code in this class, it should be
+ // in the corresponding file (url_database.cc, etc.) and called from here and
+ // from the archived_database.cc.
+
+ // When the version is too old, we just try to continue anyway, there should
+ // not be a released product that makes a database too old for us to handle.
+ int cur_version = meta_table_.GetVersionNumber();
+
+ // Put migration code here
+
+ LOG_IF(WARNING, cur_version < kCurrentVersionNumber) <<
+ "History database version " << cur_version << " is too old to handle.";
+
+ return INIT_OK;
+}
+
+} // namespace history
diff --git a/chrome/browser/history/history_database.h b/chrome/browser/history/history_database.h
new file mode 100644
index 0000000..852fef5
--- /dev/null
+++ b/chrome/browser/history/history_database.h
@@ -0,0 +1,192 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_HISTORY_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_HISTORY_DATABASE_H__
+
+#include "chrome/browser/history/download_database.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/history/starred_url_database.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/browser/history/visit_database.h"
+#include "chrome/browser/history/visitsegment_database.h"
+#include "chrome/browser/meta_table_helper.h"
+#include "chrome/common/sqlite_compiled_statement.h"
+#include "chrome/common/sqlite_utils.h"
+#include "googleurl/src/gurl.h"
+
+struct sqlite3;
+
+namespace history {
+
+// Forward declaration for the temporary migration code in Init().
+class TextDatabaseManager;
+
+// Encapsulates the SQL connection for the history database. This class holds
+// the database connection and has methods the history system (including full
+// text search) uses for writing and retrieving information.
+//
+// We try to keep most logic out of the history database; this should be seen
+// as the storage interface. Logic for manipulating this storage layer should
+// be in HistoryBackend.cc.
+class HistoryDatabase : public DownloadDatabase,
+ public StarredURLDatabase,
+ public VisitDatabase,
+ public VisitSegmentDatabase {
+ public:
+ // A simple class for scoping a history database transaction. This does not
+ // support rollback since the history database doesn't, either.
+ class TransactionScoper {
+ public:
+ TransactionScoper(HistoryDatabase* db) : db_(db) {
+ db_->BeginTransaction();
+ }
+ ~TransactionScoper() {
+ db_->CommitTransaction();
+ }
+ private:
+ HistoryDatabase* db_;
+ };
+
+ // Must call Init() to complete construction. Although it can be created on
+ // any thread, it must be destructed on the history thread for proper
+ // database cleanup.
+ HistoryDatabase();
+
+ virtual ~HistoryDatabase();
+
+ // Must call this function to complete initialization. Will return true on
+ // success. On false, no other function should be called. You may want to call
+ // BeginExclusiveMode after this when you are ready.
+ InitStatus Init(const std::wstring& history_name);
+
+ // Call to set the mode on the database to exclusive. The default locking mode
+ // is "normal" but we want to run in exclusive mode for slightly better
+ // performance since we know nobody else is using the database. This is
+ // separate from Init() since the in-memory database attaches to slurp the
+ // data out, and this can't happen in exclusive mode.
+ void BeginExclusiveMode();
+
+ // Returns the current version that we will generate history databases with.
+ static int GetCurrentVersion();
+
+ // Transactions on the history database. Use the Transaction object above
+ // for most work instead of these directly. We support nested transactions
+ // and only commit when the outermost transaction is committed. This means
+ // that it is impossible to rollback a specific transaction. We could roll
+ // back the outermost transaction if any inner one is rolled back, but it
+ // turns out we don't really need this type of integrity for the history
+ // database, so we just don't support it.
+ void BeginTransaction();
+ void CommitTransaction();
+ int transaction_nesting() const { // for debugging and assertion purposes
+ return transaction_nesting_;
+ }
+
+ // Drops all tables except the URL, starred and download tables, and recreates
+ // them from scratch. This is done to rapidly clean up stuff when deleting all
+ // history. It is faster and less likely to have problems that deleting all
+ // rows in the tables.
+ //
+ // We don't delete the downloads table, since there may be in progress
+ // downloads. We handle the download history clean up separately in:
+ // DownloadManager::RemoveDownloadsFromHistoryBetween.
+ //
+ // Returns true on success. On failure, the caller should assume that the
+ // database is invalid. There could have been an error recreating a table.
+ // This should be treated the same as an init failure, and the database
+ // should not be used any more.
+ //
+ // This will also recreate the supplimentary URL indices, since these
+ // indices won't be created automatically when using the temporary URL
+ // talbe (what the caller does right before calling this).
+ bool RecreateAllButStarAndURLTables();
+
+ // Vacuums the database. This will cause sqlite to defragment and collect
+ // unused space in the file. It can be VERY SLOW.
+ void Vacuum();
+
+ // Visit table functions ----------------------------------------------------
+
+ // Update the segment id of a visit. Return true on success.
+ bool SetSegmentID(VisitID visit_id, SegmentID segment_id);
+
+ // Query the segment ID for the provided visit. Return 0 on failure or if the
+ // visit id wasn't found.
+ SegmentID GetSegmentID(VisitID visit_id);
+
+ private:
+ // Implemented for URLDatabase.
+ virtual sqlite3* GetDB();
+ virtual SqliteStatementCache& GetStatementCache();
+
+ // Primes the sqlite cache on startup by filling it with the file in sequence
+ // until there is no more data or the cache is full. Since this is one
+ // contiguous read operation, it is much faster than letting the pages come
+ // in on-demand (which causes lots of seeks).
+ void PrimeCache();
+
+ // Sets the fields of the supplied entry from the starred select statement.
+ void FillInStarredEntry(SQLStatement* s, StarredEntry* entry);
+
+ // Migration -----------------------------------------------------------------
+
+ // Makes sure the version is up-to-date, updating if necessary. If the
+ // database is too old to migrate, the user will be notified. In this case, or
+ // for other errors, false will be returned. True means it is up-to-date and
+ // ready for use.
+ //
+ // This assumes it is called from the init function inside a transaction. It
+ // may commit the transaction and start a new one if migration requires it.
+ InitStatus EnsureCurrentVersion();
+
+ // ---------------------------------------------------------------------------
+
+ // How many nested transactions are pending? When this gets to 0, we commit.
+ int transaction_nesting_;
+
+ // The database. The closer automatically closes the deletes the db and the
+ // statement cache. These must be done in a specific order, so we don't want
+ // to rely on C++'s implicit destructors for the individual objects.
+ //
+ // The close scoper will free the database and delete the statement cache in
+ // the correct order automatically when we are destroyed.
+ DBCloseScoper db_closer_;
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+
+ MetaTableHelper meta_table_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(HistoryDatabase);
+};
+
+} // history
+
+#endif // CHROME_BROWSER_HISTORY_HISTORY_DATABASE_H__
diff --git a/chrome/browser/history/history_marshaling.h b/chrome/browser/history/history_marshaling.h
new file mode 100644
index 0000000..d1c4501
--- /dev/null
+++ b/chrome/browser/history/history_marshaling.h
@@ -0,0 +1,160 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Data structures for communication between the history service on the main
+// thread and the backend on the history thread.
+
+#ifndef CHROME_BROWSER_HISTORY_HISTORY_MARSHALING_H__
+#define CHROME_BROWSER_HISTORY_HISTORY_MARSHALING_H__
+
+#include "chrome/browser/cancelable_request.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/page_usage_data.h"
+#include "chrome/common/scoped_vector.h"
+
+namespace history {
+
+// Navigation -----------------------------------------------------------------
+
+// Marshalling structure for AddPage.
+class HistoryAddPageArgs : public base::RefCounted<HistoryAddPageArgs> {
+ public:
+ HistoryAddPageArgs(const GURL& arg_url,
+ Time arg_time,
+ const void* arg_id_scope,
+ int32 arg_page_id,
+ const GURL& arg_referrer,
+ const HistoryService::RedirectList& arg_redirects,
+ PageTransition::Type arg_transition)
+ : url(arg_url),
+ time(arg_time),
+ id_scope(arg_id_scope),
+ page_id(arg_page_id),
+ referrer(arg_referrer),
+ redirects(arg_redirects),
+ transition(arg_transition) {
+ }
+
+ GURL url;
+ Time time;
+
+ const void* id_scope;
+ int32 page_id;
+
+ GURL referrer;
+ HistoryService::RedirectList redirects;
+ PageTransition::Type transition;
+
+ private:
+ DISALLOW_EVIL_CONSTRUCTORS(HistoryAddPageArgs);
+};
+
+// Querying -------------------------------------------------------------------
+
+typedef CancelableRequest1<HistoryService::QueryURLCallback,
+ Tuple2<URLRow, VisitVector> >
+ QueryURLRequest;
+
+typedef CancelableRequest1<HistoryService::QueryHistoryCallback,
+ QueryResults>
+ QueryHistoryRequest;
+
+typedef CancelableRequest1<HistoryService::QueryRedirectsCallback,
+ HistoryService::RedirectList>
+ QueryRedirectsRequest;
+
+typedef CancelableRequest<HistoryService::GetVisitCountToHostCallback>
+ GetVisitCountToHostRequest;
+
+// Thumbnails -----------------------------------------------------------------
+
+typedef CancelableRequest<HistoryService::ThumbnailDataCallback>
+ GetPageThumbnailRequest;
+
+// Favicons -------------------------------------------------------------------
+
+typedef CancelableRequest<HistoryService::FavIconDataCallback>
+ GetFavIconRequest;
+
+// Starring -------------------------------------------------------------------
+
+typedef CancelableRequest1<HistoryService::GetStarredEntriesCallback,
+ std::vector<history::StarredEntry> >
+ GetStarredEntriesRequest;
+
+typedef CancelableRequest1<HistoryService::GetMostRecentStarredEntriesCallback,
+ std::vector<history::StarredEntry> >
+ GetMostRecentStarredEntriesRequest;
+
+typedef CancelableRequest<HistoryService::CreateStarredEntryCallback>
+ CreateStarredEntryRequest;
+
+// Downloads ------------------------------------------------------------------
+
+typedef CancelableRequest1<HistoryService::DownloadQueryCallback,
+ std::vector<DownloadCreateInfo> >
+ DownloadQueryRequest;
+
+typedef CancelableRequest<HistoryService::DownloadCreateCallback>
+ DownloadCreateRequest;
+
+typedef CancelableRequest1<HistoryService::DownloadSearchCallback,
+ std::vector<int64> >
+ DownloadSearchRequest;
+
+// Deletion --------------------------------------------------------------------
+
+typedef CancelableRequest<HistoryService::ExpireHistoryCallback>
+ ExpireHistoryRequest;
+
+// Segment usage --------------------------------------------------------------
+
+typedef CancelableRequest1<HistoryService::SegmentQueryCallback,
+ ScopedVector<PageUsageData> >
+ QuerySegmentUsageRequest;
+
+// Keyword search terms -------------------------------------------------------
+
+typedef
+ CancelableRequest1<HistoryService::GetMostRecentKeywordSearchTermsCallback,
+ std::vector<KeywordSearchTermVisit> >
+ GetMostRecentKeywordSearchTermsRequest;
+
+// Generic operations ---------------------------------------------------------
+
+// The argument here is an input value, which is the task to run on the
+// background thread. The callback is used to execute the portion of the task
+// that executes on the main thread.
+typedef CancelableRequest1<HistoryService::HistoryDBTaskCallback,
+ scoped_refptr<HistoryDBTask> >
+ HistoryDBTaskRequest;
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_HISTORY_MARSHALING_H__
diff --git a/chrome/browser/history/history_notifications.h b/chrome/browser/history/history_notifications.h
new file mode 100644
index 0000000..5cc0180
--- /dev/null
+++ b/chrome/browser/history/history_notifications.h
@@ -0,0 +1,114 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Structs that hold data used in broadcasting notifications.
+
+#ifndef CHROME_BROWSER_HISTORY_HISTORY_NOTIFICATIONS_H__
+#define CHROME_BROWSER_HISTORY_HISTORY_NOTIFICATIONS_H__
+
+#include <set>
+#include <vector>
+
+#include "googleurl/src/gurl.h"
+#include "chrome/browser/history/history_types.h"
+
+namespace history {
+
+// Base class for history notifications. This needs only a virtual destructor
+// so that the history service's broadcaster can delete it when the request
+// is complete.
+struct HistoryDetails {
+ public:
+ virtual ~HistoryDetails() {}
+};
+
+// Details for NOTIFY_HISTORY_URL_VISITED.
+struct URLVisitedDetails : public HistoryDetails {
+ URLRow row;
+};
+
+// Details for NOTIFY_HISTORY_TYPED_URLS_MODIFIED.
+struct URLsModifiedDetails : public HistoryDetails {
+ // Lists the information for each of the URLs affected.
+ std::vector<URLRow> changed_urls;
+};
+
+// Details for NOTIFY_HISTORY_URLS_DELETED.
+struct URLsDeletedDetails : public HistoryDetails {
+ // Set when all history was deleted. False means just a subset was deleted.
+ bool all_history;
+
+ // The list of unique URLs affected. This is valid only when a subset of
+ // history is deleted. When all of it is deleted, this will be empty, since
+ // we do not bother to list all URLs.
+ std::set<GURL> urls;
+};
+
+// Details for NOTIFY_HOST_DELETED_FROM_HISTORY.
+struct HostDeletedDetails : public HistoryDetails {
+ std::string host_name;
+};
+
+// Details for NOTIFY_URLS_STARRED.
+struct URLsStarredDetails : public HistoryDetails {
+
+ URLsStarredDetails(bool being_starred) : starred(being_starred) {}
+
+ // The new starred state of the list of URLs. True when they are being
+ // starred, false when they are being unstarred.
+ bool starred;
+
+ // The list of URLs that are changing.
+ std::set<GURL> changed_urls;
+
+ // The star entries that were added or removed as the result of starring
+ // the entry. This may be empty.
+ std::vector<StarredEntry> star_entries;
+};
+
+// Details for NOTIFY_STAR_ENTRY_CHANGED and others.
+struct StarredEntryDetails : public HistoryDetails {
+ StarredEntry entry;
+};
+
+// Details for NOTIFY_PAGE_PRESENTATION_INDEX_CHANGED.
+struct PresentationIndexDetails : public HistoryDetails {
+ GURL url;
+ URLID url_id;
+ int index;
+};
+
+// Details for NOTIFY_STARRED_FAVICON_CHANGED.
+struct FavIconChangeDetails : public HistoryDetails {
+ std::set<GURL> urls;
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_HISTORY_NOTIFICATIONS_H__
diff --git a/chrome/browser/history/history_querying_unittest.cc b/chrome/browser/history/history_querying_unittest.cc
new file mode 100644
index 0000000..9d5f74d
--- /dev/null
+++ b/chrome/browser/history/history_querying_unittest.cc
@@ -0,0 +1,371 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/basictypes.h"
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "chrome/browser/history/history.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+// Tests the history service for querying functionality.
+
+namespace history {
+
+namespace {
+
+struct TestEntry {
+ const char* url;
+ const wchar_t* title;
+ const int days_ago;
+ const wchar_t* body;
+ Time time; // Filled by SetUp.
+} test_entries[] = {
+ // This one is visited super long ago so it will be in a different database
+ // from the next appearance of it at the end.
+ {"http://example.com/", L"Other", 180, L"Other"},
+
+ // These are deliberately added out of chronological order. The history
+ // service should sort them by visit time when returning query results.
+ // The correct index sort order is 4 2 3 1 0.
+ {"http://www.google.com/1", L"Title 1", 10,
+ L"PAGEONE FOO some body text"},
+ {"http://www.google.com/3", L"Title 3", 8,
+ L"PAGETHREE BAR some hello world for you"},
+ {"http://www.google.com/2", L"Title 2", 9,
+ L"PAGETWO FOO some more blah blah blah"},
+
+ // A more recent visit of the first one.
+ {"http://example.com/", L"Other", 6, L"Other"},
+};
+
+// Returns true if the nth result in the given results set matches. It will
+// return false on a non-match or if there aren't enough results.
+bool NthResultIs(const QueryResults& results,
+ int n, // Result index to check.
+ int test_entry_index) { // Index of test_entries to compare.
+ if (static_cast<int>(results.size()) <= n)
+ return false;
+
+ const URLResult& result = results[n];
+
+ // Check the visit time.
+ if (result.visit_time() != test_entries[test_entry_index].time)
+ return false;
+
+ // Now check the URL & title.
+ return result.url() == GURL(test_entries[test_entry_index].url) &&
+ result.title() == std::wstring(test_entries[test_entry_index].title);
+}
+
+} // namespace
+
+class HistoryQueryTest : public testing::Test {
+ public:
+ HistoryQueryTest() {
+ }
+
+ // Acts like a synchronous call to history's QueryHistory.
+ void QueryHistory(const std::wstring& text_query,
+ const QueryOptions& options,
+ QueryResults* results) {
+ history_->QueryHistory(text_query, options, &consumer_,
+ NewCallback(this, &HistoryQueryTest::QueryHistoryComplete));
+ MessageLoop::current()->Run(); // Will go until ...Complete calls Quit.
+ results->Swap(&last_query_results_);
+ }
+
+ protected:
+ scoped_refptr<HistoryService> history_;
+
+ private:
+ virtual void SetUp() {
+ PathService::Get(base::DIR_TEMP, &history_dir_);
+ file_util::AppendToPath(&history_dir_, L"HistoryTest");
+ file_util::Delete(history_dir_, true);
+ file_util::CreateDirectory(history_dir_);
+
+ history_ = new HistoryService;
+ if (!history_->Init(history_dir_)) {
+ history_ = NULL; // Tests should notice this NULL ptr & fail.
+ return;
+ }
+
+ // Fill the test data.
+ Time now = Time::Now().LocalMidnight();
+ for (int i = 0; i < arraysize(test_entries); i++) {
+ test_entries[i].time =
+ now - (test_entries[i].days_ago * TimeDelta::FromDays(1));
+
+ // We need the ID scope and page ID so that the visit tracker can find it.
+ const void* id_scope = reinterpret_cast<void*>(1);
+ int32 page_id = i;
+ GURL url(test_entries[i].url);
+
+ history_->AddPage(url, test_entries[i].time, id_scope, page_id, GURL(),
+ PageTransition::LINK, HistoryService::RedirectList());
+ history_->SetPageTitle(url, test_entries[i].title);
+ history_->SetPageContents(url, test_entries[i].body);
+ }
+
+ // Set one of the pages starred.
+ history::StarredEntry entry;
+ entry.id = 5;
+ entry.title = L"Some starred page";
+ entry.date_added = Time::Now();
+ entry.parent_group_id = StarredEntry::BOOKMARK_BAR;
+ entry.group_id = 0;
+ entry.visual_order = 0;
+ entry.type = history::StarredEntry::URL;
+ entry.url = GURL(test_entries[0].url);
+ entry.date_group_modified = Time::Now();
+ history_->CreateStarredEntry(entry, NULL, NULL);
+ }
+
+ virtual void TearDown() {
+ if (history_.get()) {
+ history_->SetOnBackendDestroyTask(new MessageLoop::QuitTask);
+ history_->Cleanup();
+ history_ = NULL;
+ MessageLoop::current()->Run(); // Wait for the other thread.
+ }
+ file_util::Delete(history_dir_, true);
+ }
+
+ void QueryHistoryComplete(HistoryService::Handle, QueryResults* results) {
+ results->Swap(&last_query_results_);
+ MessageLoop::current()->Quit(); // Will return out to QueryHistory.
+ }
+
+ std::wstring history_dir_;
+
+ CancelableRequestConsumer consumer_;
+
+ // The QueryHistoryComplete callback will put the results here so QueryHistory
+ // can return them.
+ QueryResults last_query_results_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(HistoryQueryTest);
+};
+
+TEST_F(HistoryQueryTest, Basic) {
+ ASSERT_TRUE(history_.get());
+
+ QueryOptions options;
+ QueryResults results;
+
+ // First query for all of them to make sure they are there and in
+ // chronological order, most recent first.
+ QueryHistory(std::wstring(), options, &results);
+ ASSERT_EQ(5, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 4));
+ EXPECT_TRUE(NthResultIs(results, 1, 2));
+ EXPECT_TRUE(NthResultIs(results, 2, 3));
+ EXPECT_TRUE(NthResultIs(results, 3, 1));
+ EXPECT_TRUE(NthResultIs(results, 4, 0));
+
+ // Check that the starred one is marked starred.
+ EXPECT_TRUE(results[4].starred());
+
+ // Next query a time range. The beginning should be inclusive, the ending
+ // should be exclusive.
+ options.begin_time = test_entries[3].time;
+ options.end_time = test_entries[2].time;
+ QueryHistory(std::wstring(), options, &results);
+ EXPECT_EQ(1, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 3));
+}
+
+// Tests max_count feature for basic (non-Full Text Search) queries.
+TEST_F(HistoryQueryTest, BasicCount) {
+ ASSERT_TRUE(history_.get());
+
+ QueryOptions options;
+ QueryResults results;
+
+ // Query all time but with a limit on the number of entries. We should
+ // get the N most recent entries.
+ options.max_count = 2;
+ QueryHistory(std::wstring(), options, &results);
+ EXPECT_EQ(2, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 4));
+ EXPECT_TRUE(NthResultIs(results, 1, 2));
+}
+
+// Tests duplicate collapsing and not in non-Full Text Search situations.
+TEST_F(HistoryQueryTest, BasicDupes) {
+ ASSERT_TRUE(history_.get());
+
+ QueryOptions options;
+ QueryResults results;
+
+ // We did the query for no collapsing in the "Basic" test above, so here we
+ // only test collapsing.
+ options.most_recent_visit_only = true;
+ QueryHistory(std::wstring(), options, &results);
+ EXPECT_EQ(4, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 4));
+ EXPECT_TRUE(NthResultIs(results, 1, 2));
+ EXPECT_TRUE(NthResultIs(results, 2, 3));
+ EXPECT_TRUE(NthResultIs(results, 3, 1));
+}
+
+// This does most of the same tests above, but searches for a FTS string that
+// will match the pages in question. This will trigger a different code path.
+TEST_F(HistoryQueryTest, FTS) {
+ ASSERT_TRUE(history_.get());
+
+ QueryOptions options;
+ QueryResults results;
+
+ // Query all of them to make sure they are there and in order. Note that
+ // this query will return the starred item twice since we requested all
+ // starred entries and no de-duping.
+ QueryHistory(std::wstring(L"some"), options, &results);
+ EXPECT_EQ(4, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 4)); // Starred item.
+ EXPECT_TRUE(NthResultIs(results, 1, 2));
+ EXPECT_TRUE(NthResultIs(results, 2, 3));
+ EXPECT_TRUE(NthResultIs(results, 3, 1));
+
+ // Do a query that should only match one of them.
+ QueryHistory(std::wstring(L"PAGETWO"), options, &results);
+ EXPECT_EQ(1, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 3));
+
+ // Next query a time range. The beginning should be inclusive, the ending
+ // should be exclusive.
+ options.begin_time = test_entries[1].time;
+ options.end_time = test_entries[3].time;
+ options.include_all_starred = false;
+ QueryHistory(std::wstring(L"some"), options, &results);
+ EXPECT_EQ(1, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 1));
+}
+
+// Searches titles.
+TEST_F(HistoryQueryTest, FTSTitle) {
+ ASSERT_TRUE(history_.get());
+
+ QueryOptions options;
+ QueryResults results;
+
+ // Query all time but with a limit on the number of entries. We should
+ // get the N most recent entries.
+ QueryHistory(std::wstring(L"title"), options, &results);
+ EXPECT_EQ(3, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 2));
+ EXPECT_TRUE(NthResultIs(results, 1, 3));
+ EXPECT_TRUE(NthResultIs(results, 2, 1));
+}
+
+// Tests max_count feature for Full Text Search queries.
+TEST_F(HistoryQueryTest, FTSCount) {
+ ASSERT_TRUE(history_.get());
+
+ QueryOptions options;
+ QueryResults results;
+
+ // Query all time but with a limit on the number of entries. We should
+ // get the N most recent entries.
+ options.max_count = 2;
+ QueryHistory(std::wstring(L"some"), options, &results);
+ EXPECT_EQ(3, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 4));
+ EXPECT_TRUE(NthResultIs(results, 1, 2));
+ EXPECT_TRUE(NthResultIs(results, 2, 3));
+
+ // Now query a subset of the pages and limit by N items. "FOO" should match
+ // the 2nd & 3rd pages, but we should only get the 3rd one because of the one
+ // page max restriction.
+ options.max_count = 1;
+ QueryHistory(std::wstring(L"FOO"), options, &results);
+ EXPECT_EQ(1, results.size());
+ EXPECT_TRUE(NthResultIs(results, 0, 3));
+}
+
+// Tests that FTS queries can find URLs when they exist only in the archived
+// database. This also tests that imported URLs can be found, since we use
+// AddPageWithDetails just like the importer.
+TEST_F(HistoryQueryTest, FTSArchived) {
+ ASSERT_TRUE(history_.get());
+
+ std::vector<URLRow> urls_to_add;
+
+ URLRow row1(GURL("http://foo.bar/"));
+ row1.set_title(L"archived title");
+ row1.set_last_visit(Time::Now() - TimeDelta::FromDays(365));
+ urls_to_add.push_back(row1);
+
+ URLRow row2(GURL("http://foo.bar/"));
+ row2.set_title(L"nonarchived title");
+ row2.set_last_visit(Time::Now());
+ urls_to_add.push_back(row2);
+
+ history_->AddPagesWithDetails(urls_to_add);
+
+ QueryOptions options;
+ QueryResults results;
+
+ // Query all time. The title we get should be the one in the full text
+ // database and not the most current title (since otherwise highlighting in
+ // the title might be wrong).
+ QueryHistory(std::wstring(L"archived"), options, &results);
+ ASSERT_EQ(1, results.size());
+ EXPECT_TRUE(row1.url() == results[0].url());
+ EXPECT_TRUE(row1.title() == results[0].title());
+}
+
+/* TODO(brettw) re-enable this. It is commented out because the current history
+ code prohibits adding more than one indexed page with the same URL. When we
+ have tiered history, there could be a dupe in the archived history which
+ won't get picked up by the deletor and it can happen again. When this is the
+ case, we should fix this test to duplicate that situation.
+
+// Tests duplicate collapsing and not in Full Text Search situations.
+TEST_F(HistoryQueryTest, FTSDupes) {
+ ASSERT_TRUE(history_.get());
+
+ QueryOptions options;
+ QueryResults results;
+
+ // First do the search with collapsing.
+ QueryHistory(std::wstring(L"Other"), options, &results);
+ EXPECT_EQ(2, results.urls().size());
+ EXPECT_TRUE(NthResultIs(results, 0, 4));
+ EXPECT_TRUE(NthResultIs(results, 1, 0));
+
+ // Now with collapsing.
+ options.most_recent_visit_only = true;
+ QueryHistory(std::wstring(L"Other"), options, &results);
+ EXPECT_EQ(1, results.urls().size());
+ EXPECT_TRUE(NthResultIs(results, 0, 4));
+}
+*/
+
+} // namespace history \ No newline at end of file
diff --git a/chrome/browser/history/history_types.cc b/chrome/browser/history/history_types.cc
new file mode 100644
index 0000000..920b0bc
--- /dev/null
+++ b/chrome/browser/history/history_types.cc
@@ -0,0 +1,260 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <limits>
+
+#include "chrome/browser/history/history_types.h"
+
+namespace history {
+
+// URLRow ----------------------------------------------------------------------
+
+void URLRow::Swap(URLRow* other) {
+ std::swap(id_, other->id_);
+ url_.Swap(&other->url_);
+ title_.swap(other->title_);
+ std::swap(visit_count_, other->visit_count_);
+ std::swap(typed_count_, other->typed_count_);
+ std::swap(last_visit_, other->last_visit_);
+ std::swap(hidden_, other->hidden_);
+ std::swap(star_id_, other->star_id_);
+ std::swap(favicon_id_, other->favicon_id_);
+}
+
+void URLRow::Initialize() {
+ id_ = 0;
+ visit_count_ = false;
+ typed_count_ = false;
+ last_visit_ = Time();
+ hidden_ = false;
+ favicon_id_ = 0;
+ star_id_ = 0;
+}
+
+// VisitRow --------------------------------------------------------------------
+
+VisitRow::VisitRow()
+ : visit_id(0),
+ url_id(0),
+ referring_visit(0),
+ transition(PageTransition::LINK),
+ segment_id(0),
+ is_indexed(false) {
+}
+
+VisitRow::VisitRow(URLID arg_url_id,
+ Time arg_visit_time,
+ VisitID arg_referring_visit,
+ PageTransition::Type arg_transition,
+ SegmentID arg_segment_id)
+ : visit_id(0),
+ url_id(arg_url_id),
+ visit_time(arg_visit_time),
+ referring_visit(arg_referring_visit),
+ transition(arg_transition),
+ segment_id(arg_segment_id),
+ is_indexed(false) {
+}
+
+// StarredEntry ----------------------------------------------------------------
+
+StarredEntry::StarredEntry()
+ : id(0),
+ parent_group_id(0),
+ group_id(0),
+ visual_order(0),
+ type(URL),
+ url_id(0) {
+}
+
+void StarredEntry::Swap(StarredEntry* other) {
+ std::swap(id, other->id);
+ title.swap(other->title);
+ std::swap(date_added, other->date_added);
+ std::swap(parent_group_id, other->parent_group_id);
+ std::swap(group_id, other->group_id);
+ std::swap(visual_order, other->visual_order);
+ std::swap(type, other->type);
+ url.Swap(&other->url);
+ std::swap(url_id, other->url_id);
+ std::swap(date_group_modified, other->date_group_modified);
+}
+
+// URLResult -------------------------------------------------------------------
+
+void URLResult::Swap(URLResult* other) {
+ URLRow::Swap(other);
+ std::swap(visit_time_, other->visit_time_);
+ snippet_.Swap(&other->snippet_);
+ title_match_positions_.swap(other->title_match_positions_);
+ starred_entry_.Swap(&other->starred_entry_);
+}
+
+// QueryResults ----------------------------------------------------------------
+
+QueryResults::QueryResults() {
+}
+
+QueryResults::~QueryResults() {
+ // Free all the URL objects.
+ STLDeleteContainerPointers(results_.begin(), results_.end());
+}
+
+const size_t* QueryResults::MatchesForURL(const GURL& url,
+ size_t* num_matches) const {
+ URLToResultIndices::const_iterator found = url_to_results_.find(url);
+ if (found == url_to_results_.end()) {
+ if (num_matches)
+ *num_matches = 0;
+ return NULL;
+ }
+
+ // All entries in the map should have at least one index, otherwise it
+ // shouldn't be in the map.
+ DCHECK(found->second->size() > 0);
+ if (num_matches)
+ *num_matches = found->second->size();
+ return &found->second->front();
+}
+
+void QueryResults::Swap(QueryResults* other) {
+ std::swap(first_time_searched_, other->first_time_searched_);
+ results_.swap(other->results_);
+ url_to_results_.swap(other->url_to_results_);
+}
+
+void QueryResults::AppendURLBySwapping(URLResult* result) {
+ URLResult* new_result = new URLResult;
+ new_result->Swap(result);
+
+ results_.push_back(new_result);
+ AddURLUsageAtIndex(new_result->url(), results_.size() - 1);
+}
+
+void QueryResults::AppendResultsBySwapping(QueryResults* other,
+ bool remove_dupes) {
+ if (remove_dupes) {
+ // Delete all entries in the other array that are already in this one.
+ for (size_t i = 0; i < results_.size(); i++)
+ other->DeleteURL(results_[i]->url());
+ }
+
+ if (first_time_searched_ > other->first_time_searched_)
+ std::swap(first_time_searched_, other->first_time_searched_);
+
+ for (size_t i = 0; i < other->results_.size(); i++) {
+ // Just transfer pointer ownership.
+ results_.push_back(other->results_[i]);
+ AddURLUsageAtIndex(results_.back()->url(), results_.size() - 1);
+ }
+
+ // We just took ownerwhip of all the results in the input vector.
+ other->results_.clear();
+ other->url_to_results_.clear();
+}
+
+void QueryResults::DeleteURL(const GURL& url) {
+ // Delete all instances of this URL. We re-query each time since each
+ // mutation will cause the indices to change.
+ while (const size_t* match_indices = MatchesForURL(url, NULL))
+ DeleteRange(*match_indices, *match_indices);
+}
+
+void QueryResults::DeleteRange(size_t begin, size_t end) {
+ DCHECK(begin <= end && begin < size() && end < size());
+
+ // First delete the pointers in the given range and store all the URLs that
+ // were modified. We will delete references to these later.
+ std::set<GURL> urls_modified;
+ for (size_t i = begin; i <= end; i++) {
+ urls_modified.insert(results_[i]->url());
+ delete results_[i];
+ results_[i] = NULL;
+ }
+
+ // Now just delete that range in the vector en masse (the STL ending is
+ // exclusive, while ours is inclusive, hence the +1).
+ results_.erase(results_.begin() + begin, results_.begin() + end + 1);
+
+ // Delete the indicies referencing the deleted entries.
+ for (std::set<GURL>::const_iterator url = urls_modified.begin();
+ url != urls_modified.end(); ++url) {
+ URLToResultIndices::iterator found = url_to_results_.find(*url);
+ if (found == url_to_results_.end()) {
+ NOTREACHED();
+ continue;
+ }
+
+ // Need a signed loop type since we do -- which may take us to -1.
+ for (int match = 0; match < static_cast<int>(found->second->size());
+ match++) {
+ if (found->second[match] >= begin && found->second[match] <= end) {
+ // Remove this referece from the list.
+ found->second->erase(found->second->begin() + match);
+ match--;
+
+ }
+ }
+
+ // Clear out an empty lists if we just made one.
+ if (found->second->empty())
+ url_to_results_.erase(found);
+ }
+
+ // Shift all other indices over to account for the removed ones.
+ AdjustResultMap(end + 1, std::numeric_limits<size_t>::max(),
+ -static_cast<ptrdiff_t>(end - begin + 1));
+}
+
+void QueryResults::AddURLUsageAtIndex(const GURL& url, size_t index) {
+ URLToResultIndices::iterator found = url_to_results_.find(url);
+ if (found != url_to_results_.end()) {
+ // The URL is already in the list, so we can just append the new index.
+ found->second->push_back(index);
+ return;
+ }
+
+ // Need to add a new entry for this URL.
+ StackVector<size_t, 4> new_list;
+ new_list->push_back(index);
+ url_to_results_[url] = new_list;
+}
+
+void QueryResults::AdjustResultMap(size_t begin, size_t end, ptrdiff_t delta) {
+ for (URLToResultIndices::iterator i = url_to_results_.begin();
+ i != url_to_results_.end(); ++i) {
+ for (size_t match = 0; match < i->second->size(); match++) {
+ size_t match_index = i->second[match];
+ if (match_index >= begin && match_index <= end)
+ i->second[match] += delta;
+ }
+ }
+}
+
+} // namespace history
diff --git a/chrome/browser/history/history_types.h b/chrome/browser/history/history_types.h
new file mode 100644
index 0000000..738eda02
--- /dev/null
+++ b/chrome/browser/history/history_types.h
@@ -0,0 +1,564 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_HISTORY_TYPES_H__
+#define CHROME_BROWSER_HISTORY_HISTORY_TYPES_H__
+
+#include <algorithm>
+#include <map>
+#include <set>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/gfx/rect.h"
+#include "base/ref_counted.h"
+#include "base/stack_container.h"
+#include "base/time.h"
+#include "chrome/browser/history/snippet.h"
+#include "chrome/common/page_transition_types.h"
+#include "chrome/common/stl_util-inl.h"
+#include "googleurl/src/gurl.h"
+
+namespace history {
+
+// Forward declaration for friend statements.
+class HistoryBackend;
+class URLDatabase;
+
+typedef int64 StarID; // Unique identifier for star entries.
+typedef int64 UIStarID; // Identifier for star entries that come from the UI.
+typedef int64 DownloadID; // Identifier for a download.
+typedef int64 FavIconID; // For FavIcons.
+typedef int64 SegmentID; // URL segments for the most visited view.
+
+// Used as the return value for some databases' init function.
+enum InitStatus {
+ INIT_OK,
+
+ // Some error, usually I/O related opening the file.
+ INIT_FAILURE,
+
+ // The database is from a future version of the app and can not be read.
+ INIT_TOO_NEW,
+};
+
+// URLRow ---------------------------------------------------------------------
+
+typedef int64 URLID;
+
+// Holds all information globally associated with one URL (one row in the
+// URL table).
+//
+// This keeps track of dirty bits, which are currently unused:
+//
+// TODO(brettw) the dirty bits are broken in a number of respects. First, the
+// database will want to update them on a const object, so they need to be
+// mutable.
+//
+// Second, there is a problem copying. If you make a copy of this structure
+// (as we allow since we put this into vectors in various places) then the
+// dirty bits will not be in sync for these copies.
+class URLRow {
+ public:
+ URLRow() {
+ Initialize();
+ }
+ explicit URLRow(const GURL& url) : url_(url) {
+ // Initialize will not set the URL, so our initialization above will stay.
+ Initialize();
+ }
+
+ URLID id() const { return id_; }
+ const GURL& url() const { return url_; }
+
+ const std::wstring& title() const {
+ return title_;
+ }
+ void set_title(const std::wstring& title) {
+ // The title is frequently set to the same thing, so we don't bother
+ // updating unless the string has changed.
+ if (title != title_) {
+ title_ = title;
+ }
+ }
+
+ int visit_count() const {
+ return visit_count_;
+ }
+ void set_visit_count(int visit_count) {
+ visit_count_ = visit_count;
+ }
+
+ int typed_count() const {
+ return typed_count_;
+ }
+ void set_typed_count(int typed_count) {
+ typed_count_ = typed_count;
+ }
+
+ Time last_visit() const {
+ return last_visit_;
+ }
+ void set_last_visit(Time last_visit) {
+ last_visit_ = last_visit;
+ }
+
+ bool hidden() const {
+ return hidden_;
+ }
+ void set_hidden(bool hidden) {
+ hidden_ = hidden;
+ }
+
+ StarID star_id() const { return star_id_; }
+ void set_star_id(StarID star_id) {
+ star_id_ = star_id;
+ }
+
+ // Simple boolean query to see if the page is starred.
+ bool starred() const { return (star_id_ != 0); }
+
+ // ID of the favicon. A value of 0 means the favicon isn't known yet.
+ FavIconID favicon_id() const { return favicon_id_; }
+ void set_favicon_id(FavIconID favicon_id) {
+ favicon_id_ = favicon_id;
+ }
+
+ // Swaps the contents of this URLRow with another, which allows it to be
+ // destructively copied without memory allocations.
+ // (Virtual because it's overridden by URLResult.)
+ virtual void Swap(URLRow* other);
+
+ private:
+ // This class writes directly into this structure and clears our dirty bits
+ // when reading out of the DB.
+ friend URLDatabase;
+ friend HistoryBackend;
+
+ // Initializes all values that need initialization to their defaults.
+ // This excludes objects which autoinitialize such as strings.
+ void Initialize();
+
+ // The row ID of this URL. Immutable except for the database which sets it
+ // when it pulls them out.
+ URLID id_;
+
+ // The URL of this row. Immutable except for the database which sets it
+ // when it pulls them out. If clients want to change it, they must use
+ // the constructor to make a new one.
+ GURL url_;
+
+ std::wstring title_;
+
+ // Total number of times this URL has been visited.
+ int visit_count_;
+
+ // Number of times this URL has been manually entered in the URL bar.
+ int typed_count_;
+
+ // The date of the last visit of this URL, which saves us from having to
+ // loop up in the visit table for things like autocomplete and expiration.
+ Time last_visit_;
+
+ // Indicates this entry should now be shown in typical UI or queries, this
+ // is usually for subframes.
+ bool hidden_;
+
+ // ID of the starred entry.
+ //
+ // NOTE: This is ignored by Add/UpdateURL. To modify the starred state you
+ // must invoke SetURLStarred.
+ // TODO: revisit this to see if this limitation can be removed.
+ StarID star_id_;
+
+ // The ID of the favicon for this url.
+ FavIconID favicon_id_;
+
+ // We support the implicit copy constuctor and operator=.
+};
+
+// VisitRow -------------------------------------------------------------------
+
+typedef int64 VisitID;
+
+// Holds all information associated with a specific visit. A visit holds time
+// and referrer information for one time a URL is visited.
+class VisitRow {
+ public:
+ VisitRow();
+ VisitRow(URLID arg_url_id,
+ Time arg_visit_time,
+ VisitID arg_referring_visit,
+ PageTransition::Type arg_transition,
+ SegmentID arg_segment_id);
+
+ // ID of this row (visit ID, used a a referrer for other visits).
+ VisitID visit_id;
+
+ // Row ID into the URL table of the URL that this page is.
+ URLID url_id;
+
+ Time visit_time;
+
+ // Indicates another visit that was the referring page for this one.
+ // 0 indicates no referrer.
+ VisitID referring_visit;
+
+ // A combination of bits from PageTransition.
+ PageTransition::Type transition;
+
+ // The segment id (see visitsegment_database.*).
+ // If 0, the segment id is null in the table.
+ SegmentID segment_id;
+
+ // True when this visit has indexed data for it. We try to keep this in sync
+ // with the full text index: when we add or remove things from there, we will
+ // update the visit table as well. However, that file could get deleted, or
+ // out of sync in various ways, so this flag should be false when things
+ // change.
+ bool is_indexed;
+
+ // Compares two visits based on dates, for sorting.
+ bool operator<(const VisitRow& other) {
+ return visit_time < other.visit_time;
+ }
+
+ // We allow the implicit copy constuctor and operator=.
+};
+
+// We pass around vectors of visits a lot
+typedef std::vector<VisitRow> VisitVector;
+
+// Favicons -------------------------------------------------------------------
+
+// Used by the importer to set favicons for imported bookmarks.
+struct ImportedFavIconUsage {
+ // The URL of the favicon.
+ GURL favicon_url;
+
+ // The raw png-encoded data.
+ std::vector<unsigned char> png_data;
+
+ // The list of URLs using this favicon.
+ std::set<GURL> urls;
+};
+
+// PageVisit ------------------------------------------------------------------
+
+// Represents a simplified version of a visit for external users. Normally,
+// views are only interested in the time, and not the other information
+// associated with a VisitRow.
+struct PageVisit {
+ URLID page_id;
+ Time visit_time;
+};
+
+// StarredEntry ---------------------------------------------------------------
+
+// StarredEntry represents either a starred page, or a star grouping (where
+// a star grouping consists of child starred entries). Use the type to
+// determine the type of a particular entry.
+//
+// The database internally uses the id field to uniquely identify a starred
+// entry. On the other hand, the UI, which is anything routed through
+// HistoryService and HistoryBackend (including BookmarkBarView), uses the
+// url field to uniquely identify starred entries of type URL and the group_id
+// field to uniquely identify starred entries of type USER_GROUP. For example,
+// HistoryService::UpdateStarredEntry identifies the entry by url (if the
+// type is URL) or group_id (if the type is not URL).
+struct StarredEntry {
+ enum Type {
+ // Type represents a starred URL (StarredEntry).
+ URL,
+
+ // The bookmark bar grouping.
+ BOOKMARK_BAR,
+
+ // User created group.
+ USER_GROUP,
+
+ // The "other bookmarks" folder that holds uncategorized bookmarks.
+ OTHER
+ };
+
+ StarredEntry();
+
+ void Swap(StarredEntry* other);
+
+ // Unique identifier of this entry.
+ StarID id;
+
+ // Title.
+ std::wstring title;
+
+ // When this was added.
+ Time date_added;
+
+ // Group ID of the star group this entry is in. If 0, this entry is not
+ // in a star group.
+ UIStarID parent_group_id;
+
+ // Unique identifier for groups. This is assigned by the UI.
+ //
+ // WARNING: this is NOT the same as id, id is assigned by the database,
+ // this is assigned by the UI. See note about StarredEntry for more info.
+ UIStarID group_id;
+
+ // Visual order within the parent. Only valid if group_id is not 0.
+ int visual_order;
+
+ // Type of this entry (see enum).
+ Type type;
+
+ // If type == URL, this is the URL of the page that was starred.
+ GURL url;
+
+ // If type == URL, this is the ID of the URL of the primary page that was
+ // starred.
+ history::URLID url_id;
+
+ // Time the entry was last modified. This is only used for groups and
+ // indicates the last time a URL was added as a child to the group.
+ Time date_group_modified;
+};
+
+// URLResult -------------------------------------------------------------------
+
+class URLResult : public URLRow {
+ public:
+ URLResult() {}
+ URLResult(const GURL& url, Time visit_time)
+ : URLRow(url),
+ visit_time_(visit_time) {
+ }
+
+ Time visit_time() const { return visit_time_; }
+ void set_visit_time(Time visit_time) { visit_time_ = visit_time; }
+
+ const Snippet& snippet() const { return snippet_; }
+
+ // If this is a title match, title_match_positions contains an entry for
+ // every word in the title that matched one of the query parameters. Each
+ // entry contains the start and end of the match.
+ const Snippet::MatchPositions& title_match_positions() const {
+ return title_match_positions_;
+ }
+
+ virtual void Swap(URLResult* other);
+
+ // Returns the starred entry for this url. This is only set if the query
+ // was configured to search for starred only entries (only_starred is true).
+ const StarredEntry& starred_entry() const { return starred_entry_; }
+ void ResetStarredEntry() { starred_entry_ = StarredEntry(); }
+
+ private:
+ friend class HistoryBackend;
+
+ // The time that this result corresponds to.
+ Time visit_time_;
+
+ // When setting, these values are set directly by the HistoryBackend.
+ Snippet snippet_;
+ Snippet::MatchPositions title_match_positions_;
+
+ // See comment above getter.
+ StarredEntry starred_entry_;
+
+ // We support the implicit copy constructor and operator=.
+};
+
+// QueryResults ----------------------------------------------------------------
+
+// Encapsulates the results of a history query. It supports an ordered list of
+// URLResult objects, plus an efficient way of looking up the index of each time
+// a given URL appears in those results.
+class QueryResults {
+ public:
+ QueryResults();
+ ~QueryResults();
+
+ // Indicates the first time that the query includes results for (queries are
+ // clipped at the beginning, so it will always include to the end of the time
+ // queried).
+ //
+ // If the number of results was clipped as a result of the max count, this
+ // will be the time of the first query returned. If there were fewer results
+ // than we were allowed to return, this represents the first date considered
+ // in the query (this will be before the first result if there was time
+ // queried with no results).
+ //
+ // TODO(brettw): bug 1203054: This field is not currently set properly! Do
+ // not use until the bug is fixed.
+ Time first_time_searched() const { return first_time_searched_; }
+ void set_first_time_searched(Time t) { first_time_searched_ = t; }
+ // Note: If you need end_time_searched, it can be added.
+
+ size_t size() const { return results_.size(); }
+
+ URLResult& operator[](size_t i) { return *results_[i]; }
+ const URLResult& operator[](size_t i) const { return *results_[i]; }
+
+ // Returns a pointer to the beginning of an array of all matching indices
+ // for entries with the given URL. The array will be |*num_matches| long.
+ // |num_matches| can be NULL if the caller is not interested in the number of
+ // results (commonly it will only be interested in the first one and can test
+ // the pointer for NULL).
+ //
+ // When there is no match, it will return NULL and |*num_matches| will be 0.
+ const size_t* MatchesForURL(const GURL& url, size_t* num_matches) const;
+
+ // Swaps the current result with another. This allows ownership to be
+ // efficiently transferred without copying.
+ void Swap(QueryResults* other);
+
+ // Adds the given result to the map, using swap() on the members to avoid
+ // copying (there are a lot of strings and vectors). This means the parameter
+ // object will be cleared after this call.
+ void AppendURLBySwapping(URLResult* result);
+
+ // Appends a new result set to the other. The |other| results will be
+ // destroyed because the pointer ownership will just be transferred. When
+ // |remove_dupes| is set, each URL that appears in this array will be removed
+ // from the |other| array before appending.
+ void AppendResultsBySwapping(QueryResults* other, bool remove_dupes);
+
+ // Removes all instances of the given URL from the result set.
+ void DeleteURL(const GURL& url);
+
+ // Deletes the given range of items in the result set.
+ void DeleteRange(size_t begin, size_t end);
+
+ private:
+ typedef std::vector<URLResult*> URLResultVector;
+
+ // Maps the given URL to a list of indices into results_ which identify each
+ // time an entry with that URL appears. Normally, each URL will have one or
+ // very few indices after it, so we optimize this to use statically allocated
+ // memory when possible.
+ typedef std::map<GURL, StackVector<size_t, 4> > URLToResultIndices;
+
+ // Inserts an entry into the |url_to_results_| map saying that the given URL
+ // is at the given index in the results_.
+ void AddURLUsageAtIndex(const GURL& url, size_t index);
+
+ // Adds |delta| to each index in url_to_results_ in the range [begin,end]
+ // (this is inclusive). This is used when inserting or deleting.
+ void AdjustResultMap(size_t begin, size_t end, ptrdiff_t delta);
+
+ Time first_time_searched_;
+
+ // The ordered list of results. The pointers inside this are owned by this
+ // QueryResults object.
+ URLResultVector results_;
+
+ // Maps URLs to entries in results_.
+ URLToResultIndices url_to_results_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(QueryResults);
+};
+
+// QueryOptions ----------------------------------------------------------------
+
+struct QueryOptions {
+ QueryOptions()
+ : most_recent_visit_only(false),
+ only_starred(false),
+ include_all_starred(true),
+ max_count(0) {
+ }
+
+ // The time range to search for matches in.
+ //
+ // For text search queries, this will match only the most recent visit of the
+ // URL. If the URL was visited in the given time period, but has also been
+ // visited more recently than that, it will not be returned. When the text
+ // query is empty, this will return all URLs visited in the time range.
+ //
+ // As a special case, if both times are is_null(), then the entire database
+ // will be searched. However, if you set one, you must set the other.
+ //
+ // The beginning is inclusive and the ending is exclusive.
+ Time begin_time;
+ Time end_time;
+
+ // Sets the query time to the last |days_ago| days to the present time.
+ void SetRecentDayRange(int days_ago) {
+ end_time = Time::Now();
+ begin_time = end_time - TimeDelta::FromDays(days_ago);
+ }
+
+ // When set, only one visit for each URL will be returned, which will be the
+ // most recent one in the result set. When false, each URL may have multiple
+ // visit entries corresponding to each time the URL was visited in the given
+ // time range.
+ //
+ // Defaults to false (all visits).
+ bool most_recent_visit_only;
+
+ // Indicates if only starred items should be matched.
+ //
+ // Defaults to false.
+ bool only_starred;
+
+ // When true, and we're doing a full text query, starred entries that have
+ // never been visited or have been visited outside of the given time range
+ // will also be included in the results. These items will appear at the
+ // beginning of the result set. Non-full-text queries won't check this flag
+ // and will never return unvisited bookmarks.
+ //
+ // When false, full text queries will not return unvisited bookmarks, they
+ // will only be included when they were visited in the given time range.
+ //
+ // You probably want to use this in conjunction with most_recent_visit_only
+ // since it will cause duplicates otherwise.
+ //
+ // Defaults to true.
+ bool include_all_starred;
+
+ // The maximum number of results to return. The results will be sorted with
+ // the most recent first, so older results may not be returned if there is not
+ // enough room. When 0, this will return everything (the default).
+ int max_count;
+};
+
+// KeywordSearchTermVisit -----------------------------------------------------
+
+// KeywordSearchTermVisit is returned from GetMostRecentKeywordSearchTerms. It
+// gives the time and search term of the keyword visit.
+struct KeywordSearchTermVisit {
+ // The time of the visit.
+ Time time;
+
+ // The search term that was used.
+ std::wstring term;
+};
+
+} // history
+
+#endif // CHROME_BROWSER_HISTORY_HISTORY_TYPES_H__
diff --git a/chrome/browser/history/history_types_unittest.cc b/chrome/browser/history/history_types_unittest.cc
new file mode 100644
index 0000000..2b0b90c
--- /dev/null
+++ b/chrome/browser/history/history_types_unittest.cc
@@ -0,0 +1,194 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/history_types.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace history {
+
+namespace {
+
+// Validates the consistency of the given history result. We just make sure
+// that the URL rows match the indices structure. The unit tests themselves
+// test the index structure to verify things are in the right order, so we
+// don't need to.
+void CheckHistoryResultConsistency(const QueryResults& result) {
+ for (size_t i = 0; i < result.size(); i++) {
+ size_t match_count;
+ const size_t* matches = result.MatchesForURL(result[i].url(), &match_count);
+
+ bool found = false;
+ for (size_t match = 0; match < match_count; match++) {
+ if (matches[match] == i) {
+ found = true;
+ break;
+ }
+ }
+
+ EXPECT_TRUE(found) << "The URL had no index referring to it.";
+ }
+}
+
+static const char kURL1[] = "http://www.google.com/";
+static const char kURL2[] = "http://news.google.com/";
+static const char kURL3[] = "http://images.google.com/";
+
+// Adds kURL1 twice and kURL2 once.
+void AddSimpleData(QueryResults* results) {
+ GURL url1(kURL1);
+ GURL url2(kURL2);
+ URLResult result1(url1, Time::Now());
+ URLResult result2(url1, Time::Now());
+ URLResult result3(url2, Time::Now());
+
+ // The URLResults are invalid after being inserted.
+ results->AppendURLBySwapping(&result1);
+ results->AppendURLBySwapping(&result2);
+ results->AppendURLBySwapping(&result3);
+ CheckHistoryResultConsistency(*results);
+}
+
+// Adds kURL2 once and kURL3 once.
+void AddAlternateData(QueryResults* results) {
+ GURL url2(kURL2);
+ GURL url3(kURL3);
+ URLResult result1(url2, Time::Now());
+ URLResult result2(url3, Time::Now());
+
+ // The URLResults are invalid after being inserted.
+ results->AppendURLBySwapping(&result1);
+ results->AppendURLBySwapping(&result2);
+ CheckHistoryResultConsistency(*results);
+}
+
+} // namespace
+
+// Tests insertion and deletion by range.
+TEST(HistoryQueryResult, DeleteRange) {
+ GURL url1(kURL1);
+ GURL url2(kURL2);
+ QueryResults results;
+ AddSimpleData(&results);
+
+ // Make sure the first URL is in there twice. The indices can be in either
+ // order.
+ size_t match_count;
+ const size_t* matches = results.MatchesForURL(url1, &match_count);
+ ASSERT_EQ(2, match_count);
+ EXPECT_TRUE((matches[0] == 0 && matches[1] == 1) ||
+ (matches[0] == 1 && matches[1] == 0));
+
+ // Check the second one.
+ matches = results.MatchesForURL(url2, &match_count);
+ ASSERT_EQ(1, match_count);
+ EXPECT_TRUE(matches[0] == 2);
+
+ // Delete the first instance of the first URL.
+ results.DeleteRange(0, 0);
+ CheckHistoryResultConsistency(results);
+
+ // Check the two URLs.
+ matches = results.MatchesForURL(url1, &match_count);
+ ASSERT_EQ(1, match_count);
+ EXPECT_TRUE(matches[0] == 0);
+ matches = results.MatchesForURL(url2, &match_count);
+ ASSERT_EQ(1, match_count);
+ EXPECT_TRUE(matches[0] == 1);
+
+ // Now delete everything and make sure it's deleted.
+ results.DeleteRange(0, 1);
+ EXPECT_EQ(0, results.size());
+ EXPECT_FALSE(results.MatchesForURL(url1, NULL));
+ EXPECT_FALSE(results.MatchesForURL(url2, NULL));
+}
+
+// Tests insertion and deletion by URL.
+TEST(HistoryQueryResult, ResultDeleteURL) {
+ GURL url1(kURL1);
+ GURL url2(kURL2);
+ QueryResults results;
+ AddSimpleData(&results);
+
+ // Delete the first URL.
+ results.DeleteURL(url1);
+ CheckHistoryResultConsistency(results);
+ EXPECT_EQ(1, results.size());
+
+ // The first one should be gone, and the second one should be at [0].
+ size_t match_count;
+ EXPECT_FALSE(results.MatchesForURL(url1, NULL));
+ const size_t* matches = results.MatchesForURL(url2, &match_count);
+ ASSERT_EQ(1, match_count);
+ EXPECT_TRUE(matches[0] == 0);
+
+ // Delete the second URL, there should be nothing left.
+ results.DeleteURL(url2);
+ EXPECT_EQ(0, results.size());
+ EXPECT_FALSE(results.MatchesForURL(url2, NULL));
+}
+
+TEST(HistoryQueryResult, AppendResults) {
+ GURL url1(kURL1);
+ GURL url2(kURL2);
+ GURL url3(kURL3);
+
+ // This is the base.
+ QueryResults results;
+ AddSimpleData(&results);
+
+ // Now create the appendee.
+ QueryResults appendee;
+ AddAlternateData(&appendee);
+
+ results.AppendResultsBySwapping(&appendee, true);
+ CheckHistoryResultConsistency(results);
+
+ // There should be 3 results, the second one of the appendee should be
+ // deleted because it was already in the first one and we said remove dupes.
+ ASSERT_EQ(4, results.size());
+
+ // The first URL should be unchanged in the first two spots.
+ size_t match_count;
+ const size_t* matches = results.MatchesForURL(url1, &match_count);
+ ASSERT_EQ(2, match_count);
+ EXPECT_TRUE((matches[0] == 0 && matches[1] == 1) ||
+ (matches[0] == 1 && matches[1] == 0));
+
+ // The second URL should be there once after that
+ matches = results.MatchesForURL(url2, &match_count);
+ ASSERT_EQ(1, match_count);
+ EXPECT_TRUE(matches[0] == 2);
+
+ // The third one should be after that.
+ matches = results.MatchesForURL(url3, &match_count);
+ ASSERT_EQ(1, match_count);
+ EXPECT_TRUE(matches[0] == 3);
+}
+
+} // namespace
diff --git a/chrome/browser/history/history_unittest.cc b/chrome/browser/history/history_unittest.cc
new file mode 100644
index 0000000..37b9179
--- /dev/null
+++ b/chrome/browser/history/history_unittest.cc
@@ -0,0 +1,925 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// History unit tests come in two flavors:
+//
+// 1. The more complicated style is that the unit test creates a full history
+// service. This spawns a background thread for the history backend, and
+// all communication is asynchronous. This is useful for testing more
+// complicated things or end-to-end behavior.
+//
+// 2. The simpler style is to create a history backend on this thread and
+// access it directly without a HistoryService object. This is much simpler
+// because communication is synchronous. Generally, sets should go through
+// the history backend (since there is a lot of logic) but gets can come
+// directly from the HistoryDatabase. This is because the backend generally
+// has no logic in the getter except threading stuff, which we don't want
+// to run.
+
+#include <time.h>
+#include <algorithm>
+
+#include "base/basictypes.h"
+#include "base/file_util.h"
+#include "base/message_loop.h"
+#include "base/path_service.h"
+#include "base/string_util.h"
+#include "base/task.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/download_manager.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/history_backend.h"
+#include "chrome/browser/history/history_database.h"
+#include "chrome/browser/history/in_memory_database.h"
+#include "chrome/browser/history/in_memory_history_backend.h"
+#include "chrome/browser/history/page_usage_data.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/jpeg_codec.h"
+#include "chrome/common/notification_service.h"
+#include "chrome/common/sqlite_utils.h"
+#include "chrome/common/scoped_vector.h"
+#include "chrome/common/thumbnail_score.h"
+#include "chrome/tools/profiles/thumbnail-inl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+class HistoryTest;
+
+// Specialize RunnableMethodTraits for HistoryTest so we can create callbacks.
+// None of these callbacks can outlast the test, so there is not need to retain
+// the HistoryTest object.
+template <>
+struct RunnableMethodTraits<HistoryTest> {
+ static void RetainCallee(HistoryTest* obj) { }
+ static void ReleaseCallee(HistoryTest* obj) { }
+};
+
+namespace history {
+
+namespace {
+
+// Compares the two data values. Used for comparing thumbnail data.
+bool DataEqual(const unsigned char* reference, int reference_len,
+ const std::vector<unsigned char>& data) {
+ if (reference_len != data.size())
+ return false;
+ for (int i = 0; i < reference_len; i++) {
+ if (data[i] != reference[i])
+ return false;
+ }
+ return true;
+}
+
+// The tracker uses RenderProcessHost pointers for scoping but never
+// dereferences them. We use ints because it's easier. This function converts
+// between the two.
+static void* MakeFakeHost(int id) {
+ void* host = 0;
+ memcpy(&host, &id, sizeof(id));
+ return host;
+}
+
+// Delegate class for when we create a backend without a HistoryService.
+class BackendDelegate : public HistoryBackend::Delegate {
+ public:
+ explicit BackendDelegate(HistoryTest* history_test)
+ : history_test_(history_test) {
+ }
+
+ virtual void NotifyTooNew();
+ virtual void SetInMemoryBackend(InMemoryHistoryBackend* backend);
+ virtual void BroadcastNotifications(NotificationType type,
+ HistoryDetails* details);
+
+ private:
+ HistoryTest* history_test_;
+};
+
+bool IsURLStarred(URLDatabase* db, const GURL& url) {
+ URLRow row;
+ EXPECT_TRUE(db->GetRowForURL(url, &row)) << "URL not found";
+ return row.starred();
+}
+
+} // namespace
+
+// This must be outside the anonymous namespace for the friend statement in
+// HistoryBackend to work.
+class HistoryTest : public testing::Test {
+ public:
+ HistoryTest() : history_service_(NULL), db_(NULL) {
+ }
+ ~HistoryTest() {
+ }
+
+ // Thumbnail callback: we save the data and exit the message loop so the
+ // unit test can read the data
+ void OnThumbnailDataAvailable(
+ HistoryService::Handle request_handle,
+ scoped_refptr<RefCountedBytes> jpeg_data) {
+ got_thumbnail_callback_ = true;
+ if (jpeg_data.get()) {
+ std::copy(jpeg_data->data.begin(), jpeg_data->data.end(),
+ std::back_inserter(thumbnail_data_));
+ }
+ MessageLoop::current()->Quit();
+ }
+
+ // Creates the HistoryBackend and HistoryDatabase on the current thread,
+ // assigning the values to backend_ and db_.
+ void CreateBackendAndDatabase() {
+ backend_ = new HistoryBackend(history_dir_, new BackendDelegate(this));
+ backend_->Init();
+ db_ = backend_->db_.get();
+ DCHECK(in_mem_backend_.get()) << "Mem backend should have been set by "
+ "HistoryBackend::Init";
+ }
+
+ void OnSegmentUsageAvailable(CancelableRequestProvider::Handle handle,
+ std::vector<PageUsageData*>* data) {
+ page_usage_data_->swap(*data);
+ MessageLoop::current()->Quit();
+ }
+
+ void OnDeleteURLsDone(CancelableRequestProvider::Handle handle) {
+ MessageLoop::current()->Quit();
+ }
+
+ protected:
+ friend class BackendDelegate;
+
+ // testing::Test
+ virtual void SetUp() {
+ PathService::Get(base::DIR_TEMP, &history_dir_);
+ file_util::AppendToPath(&history_dir_, L"HistoryTest");
+ file_util::Delete(history_dir_, true);
+ file_util::CreateDirectory(history_dir_);
+ }
+
+ void DeleteBackend() {
+ if (backend_) {
+ backend_->Closing();
+ backend_ = NULL;
+ }
+ }
+
+ virtual void TearDown() {
+ DeleteBackend();
+
+ if (history_service_)
+ CleanupHistoryService();
+
+ // Try to clean up the database file.
+ file_util::Delete(history_dir_, true);
+
+ // Make sure we don't have any event pending that could disrupt the next
+ // test.
+ MessageLoop::current()->PostTask(FROM_HERE, new MessageLoop::QuitTask);
+ MessageLoop::current()->Run();
+ }
+
+ void CleanupHistoryService() {
+ DCHECK(history_service_.get());
+
+ history_service_->NotifyRenderProcessHostDestruction(0);
+ history_service_->SetOnBackendDestroyTask(new MessageLoop::QuitTask);
+ history_service_->Cleanup();
+ history_service_ = NULL;
+
+ // Wait for the backend class to terminate before deleting the files and
+ // moving to the next test. Note: if this never terminates, somebody is
+ // probably leaking a reference to the history backend, so it never calls
+ // our destroy task.
+ MessageLoop::current()->Run();
+ }
+
+ int64 AddDownload(int32 state, const Time& time) {
+ DownloadCreateInfo download(L"foo-path", L"foo-url", time,
+ 0, 512, state, 0);
+ return db_->CreateDownload(download);
+ }
+
+ // Fills the query_url_row_ and query_url_visits_ structures with the
+ // information about the given URL and returns true. If the URL was not
+ // found, this will return false and those structures will not be changed.
+ bool QueryURL(HistoryService* history, const GURL& url) {
+ history->QueryURL(url, true, &consumer_,
+ NewCallback(this, &HistoryTest::SaveURLAndQuit));
+ MessageLoop::current()->Run(); // Will be exited in SaveURLAndQuit.
+ return query_url_success_;
+ }
+
+ // Callback for HistoryService::QueryURL.
+ void SaveURLAndQuit(HistoryService::Handle handle,
+ bool success,
+ const URLRow* url_row,
+ VisitVector* visit_vector) {
+ query_url_success_ = success;
+ if (query_url_success_) {
+ query_url_row_ = *url_row;
+ query_url_visits_.swap(*visit_vector);
+ } else {
+ query_url_row_ = URLRow();
+ query_url_visits_.clear();
+ }
+ MessageLoop::current()->Quit();
+ }
+
+ // Fills in saved_redirects_ with the redirect information for the given URL,
+ // returning true on success. False means the URL was not found.
+ bool QueryRedirectsFrom(HistoryService* history, const GURL& url) {
+ history->QueryRedirectsFrom(url, &consumer_,
+ NewCallback(this, &HistoryTest::OnRedirectQueryComplete));
+ MessageLoop::current()->Run(); // Will be exited in *QueryComplete.
+ return redirect_query_success_;
+ }
+
+ // Callback for QueryRedirects.
+ void OnRedirectQueryComplete(HistoryService::Handle handle,
+ GURL url,
+ bool success,
+ HistoryService::RedirectList* redirects) {
+ redirect_query_success_ = success;
+ if (redirect_query_success_)
+ saved_redirects_.swap(*redirects);
+ else
+ saved_redirects_.clear();
+ MessageLoop::current()->Quit();
+ }
+
+ void SetURLStarred(const GURL& url, bool starred) {
+ history::StarredEntry entry;
+ entry.type = history::StarredEntry::URL;
+ entry.url = url;
+ history::StarID star_id = db_->GetStarIDForEntry(entry);
+ if (star_id && !starred) {
+ // Unstar.
+ backend_->DeleteStarredEntry(star_id);
+ } else if (!star_id && starred) {
+ // Star.
+ entry.parent_group_id = HistoryService::kBookmarkBarID;
+ backend_->CreateStarredEntry(NULL, entry);
+ }
+ }
+
+ // PageUsageData vector to test segments.
+ ScopedVector<PageUsageData> page_usage_data_;
+
+ // When non-NULL, this will be deleted on tear down and we will block until
+ // the backend thread has completed. This allows tests for the history
+ // service to use this feature, but other tests to ignore this.
+ scoped_refptr<HistoryService> history_service_;
+
+ // names of the database files
+ std::wstring history_dir_;
+
+ // Set by the thumbnail callback when we get data, you should be sure to
+ // clear this before issuing a thumbnail request.
+ bool got_thumbnail_callback_;
+ std::vector<unsigned char> thumbnail_data_;
+
+ // Set by the redirect callback when we get data. You should be sure to
+ // clear this before issuing a redirect request.
+ HistoryService::RedirectList saved_redirects_;
+ bool redirect_query_success_;
+
+ // For history requests.
+ CancelableRequestConsumer consumer_;
+
+ // For saving URL info after a call to QueryURL
+ bool query_url_success_;
+ URLRow query_url_row_;
+ VisitVector query_url_visits_;
+
+ // Created via CreateBackendAndDatabase.
+ scoped_refptr<HistoryBackend> backend_;
+ scoped_ptr<InMemoryHistoryBackend> in_mem_backend_;
+ HistoryDatabase* db_; // Cached reference to the backend's database.
+};
+
+namespace {
+
+void BackendDelegate::NotifyTooNew() {
+}
+
+void BackendDelegate::SetInMemoryBackend(InMemoryHistoryBackend* backend) {
+ // Save the in-memory backend to the history test object, this happens
+ // synchronously, so we don't have to do anything fancy.
+ history_test_->in_mem_backend_.reset(backend);
+}
+
+void BackendDelegate::BroadcastNotifications(NotificationType type,
+ HistoryDetails* details) {
+ // Currently, just send the notifications directly to the in-memory database.
+ // We may want do do something more fancy in the future.
+ Details<HistoryDetails> det(details);
+ history_test_->in_mem_backend_->Observe(type,
+ Source<HistoryTest>(NULL), det);
+
+ // The backend passes ownership of the details pointer to us.
+ delete details;
+}
+
+} // namespace
+
+TEST_F(HistoryTest, ClearBrowsingData_Downloads) {
+ CreateBackendAndDatabase();
+
+ Time now = Time::Now();
+ TimeDelta one_day = TimeDelta::FromDays(1);
+ Time month_ago = now - TimeDelta::FromDays(30);
+
+ // Initially there should be nothing in the downloads database.
+ std::vector<DownloadCreateInfo> downloads;
+ db_->QueryDownloads(&downloads);
+ EXPECT_EQ(0, downloads.size());
+
+ // Keep track of these as we need to update them later during the test.
+ DownloadID in_progress, removing;
+
+ // Create one with a 0 time.
+ EXPECT_NE(0, AddDownload(DownloadItem::COMPLETE, Time()));
+ // Create one for now and +/- 1 day.
+ EXPECT_NE(0, AddDownload(DownloadItem::COMPLETE, now - one_day));
+ EXPECT_NE(0, AddDownload(DownloadItem::COMPLETE, now));
+ EXPECT_NE(0, AddDownload(DownloadItem::COMPLETE, now + one_day));
+ // Try the other three states.
+ EXPECT_NE(0, AddDownload(DownloadItem::COMPLETE, month_ago));
+ EXPECT_NE(0, in_progress = AddDownload(DownloadItem::IN_PROGRESS, month_ago));
+ EXPECT_NE(0, AddDownload(DownloadItem::CANCELLED, month_ago));
+ EXPECT_NE(0, removing = AddDownload(DownloadItem::REMOVING, month_ago));
+
+ // Test to see if inserts worked.
+ db_->QueryDownloads(&downloads);
+ EXPECT_EQ(8, downloads.size());
+
+ // Try removing from current timestamp. This should delete the one in the
+ // future and one very recent one.
+ db_->RemoveDownloadsBetween(now, Time());
+ db_->QueryDownloads(&downloads);
+ EXPECT_EQ(6, downloads.size());
+
+ // Try removing from two months ago. This should not delete items that are
+ // 'in progress' or in 'removing' state.
+ db_->RemoveDownloadsBetween(now - TimeDelta::FromDays(60), Time());
+ db_->QueryDownloads(&downloads);
+ EXPECT_EQ(3, downloads.size());
+
+ // Download manager converts to TimeT, which is lossy, so we do the same
+ // for comparison.
+ Time month_ago_lossy = Time::FromTimeT(month_ago.ToTimeT());
+
+ // Make sure the right values remain.
+ EXPECT_EQ(DownloadItem::COMPLETE, downloads[0].state);
+ EXPECT_EQ(0, downloads[0].start_time.ToInternalValue());
+ EXPECT_EQ(DownloadItem::IN_PROGRESS, downloads[1].state);
+ EXPECT_EQ(month_ago_lossy.ToInternalValue(),
+ downloads[1].start_time.ToInternalValue());
+ EXPECT_EQ(DownloadItem::REMOVING, downloads[2].state);
+ EXPECT_EQ(month_ago_lossy.ToInternalValue(),
+ downloads[2].start_time.ToInternalValue());
+
+ // Change state so we can delete the downloads.
+ EXPECT_TRUE(db_->UpdateDownload(512, DownloadItem::COMPLETE, in_progress));
+ EXPECT_TRUE(db_->UpdateDownload(512, DownloadItem::CANCELLED, removing));
+
+ // Try removing from Time=0. This should delete all.
+ db_->RemoveDownloadsBetween(Time(), Time());
+ db_->QueryDownloads(&downloads);
+ EXPECT_EQ(0, downloads.size());
+}
+
+TEST_F(HistoryTest, AddPage) {
+ scoped_refptr<HistoryService> history(new HistoryService);
+ history_service_ = history;
+ ASSERT_TRUE(history->Init(history_dir_));
+
+ // Add the page once from a child frame.
+ const GURL test_url("http://www.google.com/");
+ history->AddPage(test_url, NULL, 0, GURL(),
+ PageTransition::MANUAL_SUBFRAME,
+ HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_url));
+ EXPECT_EQ(1, query_url_row_.visit_count());
+ EXPECT_EQ(0, query_url_row_.typed_count());
+ EXPECT_TRUE(query_url_row_.hidden()); // Hidden because of child frame.
+
+ // Add the page once from the main frame (should unhide it).
+ history->AddPage(test_url, NULL, 0, GURL(), PageTransition::LINK,
+ HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_url));
+ EXPECT_EQ(2, query_url_row_.visit_count()); // Added twice.
+ EXPECT_EQ(0, query_url_row_.typed_count()); // Never typed.
+ EXPECT_FALSE(query_url_row_.hidden()); // Because loaded in main frame.
+}
+
+TEST_F(HistoryTest, AddPageSameTimes) {
+ scoped_refptr<HistoryService> history(new HistoryService);
+ history_service_ = history;
+ ASSERT_TRUE(history->Init(history_dir_));
+
+ Time now = Time::Now();
+ const GURL test_urls[] = {
+ GURL(L"http://timer.first.page/"),
+ GURL(L"http://timer.second.page/"),
+ GURL(L"http://timer.third.page/"),
+ };
+
+ // Make sure that two pages added at the same time with no intervening
+ // additions have different timestamps.
+ history->AddPage(test_urls[0], now, NULL, 0, GURL(),
+ PageTransition::LINK,
+ HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_urls[0]));
+ EXPECT_EQ(1, query_url_row_.visit_count());
+ EXPECT_TRUE(now == query_url_row_.last_visit()); // gtest doesn't like Time
+
+ history->AddPage(test_urls[1], now, NULL, 0, GURL(),
+ PageTransition::LINK,
+ HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_urls[1]));
+ EXPECT_EQ(1, query_url_row_.visit_count());
+ EXPECT_TRUE(now + TimeDelta::FromMicroseconds(1) ==
+ query_url_row_.last_visit());
+
+ // Make sure the next page, at a different time, is also correct.
+ history->AddPage(test_urls[2], now + TimeDelta::FromMinutes(1),
+ NULL, 0, GURL(),
+ PageTransition::LINK,
+ HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_urls[2]));
+ EXPECT_EQ(1, query_url_row_.visit_count());
+ EXPECT_TRUE(now + TimeDelta::FromMinutes(1) ==
+ query_url_row_.last_visit());
+}
+
+TEST_F(HistoryTest, AddRedirect) {
+ scoped_refptr<HistoryService> history(new HistoryService);
+ history_service_ = history;
+ ASSERT_TRUE(history->Init(history_dir_));
+
+ const wchar_t* first_sequence[] = {
+ L"http://first.page/",
+ L"http://second.page/"};
+ int first_count = arraysize(first_sequence);
+ HistoryService::RedirectList first_redirects;
+ for (int i = 0; i < first_count; i++)
+ first_redirects.push_back(GURL(first_sequence[i]));
+
+ // Add the sequence of pages as a server with no referrer. Note that we need
+ // to have a non-NULL page ID scope.
+ history->AddPage(first_redirects.back(), MakeFakeHost(1), 0, GURL(),
+ PageTransition::LINK, first_redirects);
+
+ // The first page should be added once with a link visit type (because we set
+ // LINK when we added the original URL, and a referrer of nowhere (0).
+ EXPECT_TRUE(QueryURL(history, first_redirects[0]));
+ EXPECT_EQ(1, query_url_row_.visit_count());
+ ASSERT_EQ(1, query_url_visits_.size());
+ __int64 first_visit = query_url_visits_[0].visit_id;
+ EXPECT_EQ(PageTransition::LINK |
+ PageTransition::CHAIN_START, query_url_visits_[0].transition);
+ EXPECT_EQ(0, query_url_visits_[0].referring_visit); // No referrer.
+
+ // The second page should be a server redirect type with a referrer of the
+ // first page.
+ EXPECT_TRUE(QueryURL(history, first_redirects[1]));
+ EXPECT_EQ(1, query_url_row_.visit_count());
+ ASSERT_EQ(1, query_url_visits_.size());
+ __int64 second_visit = query_url_visits_[0].visit_id;
+ EXPECT_EQ(PageTransition::SERVER_REDIRECT |
+ PageTransition::CHAIN_END, query_url_visits_[0].transition);
+ EXPECT_EQ(first_visit, query_url_visits_[0].referring_visit);
+
+ // Check that the redirect finding function successfully reports it.
+ saved_redirects_.clear();
+ QueryRedirectsFrom(history, first_redirects[0]);
+ ASSERT_EQ(1, saved_redirects_.size());
+ EXPECT_EQ(first_redirects[1], saved_redirects_[0]);
+
+ // Now add a client redirect from that second visit to a third, client
+ // redirects are tracked by the RenderView prior to updating history,
+ // so we pass in a CLIENT_REDIRECT qualifier to mock that behavior.
+ HistoryService::RedirectList second_redirects;
+ second_redirects.push_back(first_redirects[1]);
+ second_redirects.push_back(GURL("http://last.page/"));
+ history->AddPage(second_redirects[1], MakeFakeHost(1), 1,
+ second_redirects[0],
+ static_cast<PageTransition::Type>(PageTransition::LINK |
+ PageTransition::CLIENT_REDIRECT),
+ second_redirects);
+
+ // The last page (source of the client redirect) should NOT have an
+ // additional visit added, because it was a client redirect (normally it
+ // would). We should only have 1 left over from the first sequence.
+ EXPECT_TRUE(QueryURL(history, second_redirects[0]));
+ EXPECT_EQ(1, query_url_row_.visit_count());
+
+ // The final page should be set as a client redirect from the previous visit.
+ EXPECT_TRUE(QueryURL(history, second_redirects[1]));
+ EXPECT_EQ(1, query_url_row_.visit_count());
+ ASSERT_EQ(1, query_url_visits_.size());
+ EXPECT_EQ(PageTransition::CLIENT_REDIRECT |
+ PageTransition::CHAIN_END, query_url_visits_[0].transition);
+ EXPECT_EQ(second_visit, query_url_visits_[0].referring_visit);
+}
+
+TEST_F(HistoryTest, Typed) {
+ scoped_refptr<HistoryService> history(new HistoryService);
+ history_service_ = history;
+ ASSERT_TRUE(history->Init(history_dir_));
+
+ // Add the page once as typed.
+ const GURL test_url("http://www.google.com/");
+ history->AddPage(test_url, NULL, 0, GURL(), PageTransition::TYPED,
+ HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_url));
+
+ // We should have the same typed & visit count.
+ EXPECT_EQ(1, query_url_row_.visit_count());
+ EXPECT_EQ(1, query_url_row_.typed_count());
+
+ // Add the page again not typed.
+ history->AddPage(test_url, NULL, 0, GURL(), PageTransition::LINK,
+ HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_url));
+
+ // The second time should not have updated the typed count.
+ EXPECT_EQ(2, query_url_row_.visit_count());
+ EXPECT_EQ(1, query_url_row_.typed_count());
+
+ // Add the page again as a generated URL.
+ history->AddPage(test_url, NULL, 0, GURL(),
+ PageTransition::GENERATED, HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_url));
+
+ // This should have worked like a link click.
+ EXPECT_EQ(3, query_url_row_.visit_count());
+ EXPECT_EQ(1, query_url_row_.typed_count());
+
+ // Add the page again as a reload.
+ history->AddPage(test_url, NULL, 0, GURL(),
+ PageTransition::RELOAD, HistoryService::RedirectList());
+ EXPECT_TRUE(QueryURL(history, test_url));
+
+ // This should not have incremented any visit counts.
+ EXPECT_EQ(3, query_url_row_.visit_count());
+ EXPECT_EQ(1, query_url_row_.typed_count());
+}
+
+TEST_F(HistoryTest, SetTitle) {
+ scoped_refptr<HistoryService> history(new HistoryService);
+ history_service_ = history;
+ ASSERT_TRUE(history->Init(history_dir_));
+
+ // Add a URL.
+ const GURL existing_url(L"http://www.google.com/");
+ history->AddPage(existing_url);
+
+ // Set some title.
+ const std::wstring existing_title(L"Google");
+ history->SetPageTitle(existing_url, existing_title);
+
+ // Make sure the title got set.
+ EXPECT_TRUE(QueryURL(history, existing_url));
+ EXPECT_EQ(existing_title, query_url_row_.title());
+
+ // set a title on a nonexistent page
+ const GURL nonexistent_url(L"http://news.google.com/");
+ const std::wstring nonexistent_title(L"Google News");
+ history->SetPageTitle(nonexistent_url, nonexistent_title);
+
+ // Make sure nothing got written.
+ EXPECT_FALSE(QueryURL(history, nonexistent_url));
+ EXPECT_EQ(std::wstring(), query_url_row_.title());
+
+ // TODO(brettw) this should also test redirects, which get the title of the
+ // destination page.
+}
+
+TEST_F(HistoryTest, Segments) {
+ scoped_refptr<HistoryService> history(new HistoryService);
+ history_service_ = history;
+
+ ASSERT_TRUE(history->Init(history_dir_));
+
+ static const void* scope = static_cast<void*>(this);
+
+ // Add a URL.
+ const GURL existing_url("http://www.google.com/");
+ history->AddPage(existing_url, scope, 0, GURL(),
+ PageTransition::TYPED, HistoryService::RedirectList());
+
+ // Make sure a segment was created.
+ history->QuerySegmentUsageSince(
+ &consumer_, Time::Now() - TimeDelta::FromDays(1),
+ NewCallback(static_cast<HistoryTest*>(this),
+ &HistoryTest::OnSegmentUsageAvailable));
+
+ // Wait for processing.
+ MessageLoop::current()->Run();
+
+ EXPECT_EQ(page_usage_data_->size(), 1);
+ EXPECT_TRUE(page_usage_data_[0]->GetURL() == existing_url);
+ EXPECT_DOUBLE_EQ(3.0, page_usage_data_[0]->GetScore());
+
+ // Add a URL which doesn't create a segment.
+ const GURL link_url("http://yahoo.com/");
+ history->AddPage(link_url, scope, 0, GURL(),
+ PageTransition::LINK, HistoryService::RedirectList());
+
+ // Query again
+ history->QuerySegmentUsageSince(
+ &consumer_, Time::Now() - TimeDelta::FromDays(1),
+ NewCallback(static_cast<HistoryTest*>(this),
+ &HistoryTest::OnSegmentUsageAvailable));
+
+ // Wait for processing.
+ MessageLoop::current()->Run();
+
+ // Make sure we still have one segment.
+ EXPECT_EQ(page_usage_data_->size(), 1);
+ EXPECT_TRUE(page_usage_data_[0]->GetURL() == existing_url);
+
+ // Add a page linked from existing_url.
+ history->AddPage(GURL("http://www.google.com/foo"), scope, 3, existing_url,
+ PageTransition::LINK, HistoryService::RedirectList());
+
+ // Query again
+ history->QuerySegmentUsageSince(
+ &consumer_, Time::Now() - TimeDelta::FromDays(1),
+ NewCallback(static_cast<HistoryTest*>(this),
+ &HistoryTest::OnSegmentUsageAvailable));
+
+ // Wait for processing.
+ MessageLoop::current()->Run();
+
+ // Make sure we still have one segment.
+ EXPECT_EQ(page_usage_data_->size(), 1);
+ EXPECT_TRUE(page_usage_data_[0]->GetURL() == existing_url);
+
+ // However, the score should have increased.
+ EXPECT_GT(page_usage_data_[0]->GetScore(), 5.0);
+}
+
+// This just tests history system -> thumbnail database integration, the actual
+// thumbnail tests are in its own file.
+TEST_F(HistoryTest, Thumbnails) {
+ scoped_refptr<HistoryService> history(new HistoryService);
+ history_service_ = history;
+ ASSERT_TRUE(history->Init(history_dir_));
+
+ scoped_ptr<SkBitmap> thumbnail(
+ JPEGCodec::Decode(kGoogleThumbnail, sizeof(kGoogleThumbnail)));
+ static const double boringness = 0.25;
+
+ const GURL url("http://www.google.com/thumbnail_test/");
+ history->AddPage(url); // Must be visited before adding a thumbnail.
+ history->SetPageThumbnail(url, *thumbnail,
+ ThumbnailScore(boringness, true, true));
+
+ // Make sure we get the correct thumbnail data.
+ EXPECT_TRUE(history->GetPageThumbnail(url, &consumer_,
+ NewCallback(static_cast<HistoryTest*>(this),
+ &HistoryTest::OnThumbnailDataAvailable)));
+ thumbnail_data_.clear();
+ MessageLoop::current()->Run();
+ // Make sure we got a valid JPEG back. This isn't equivalent to
+ // being correct, but when we're roundtripping through JPEG
+ // compression and we don't have a similarity measure.
+ EXPECT_TRUE(thumbnail_data_.size());
+ scoped_ptr<SkBitmap> decoded_thumbnail(
+ JPEGCodec::Decode(&thumbnail_data_[0], thumbnail_data_.size()));
+ EXPECT_TRUE(decoded_thumbnail.get());
+
+ // Request a nonexistent thumbnail and make sure we get
+ // a callback and no data.
+ EXPECT_TRUE(history->GetPageThumbnail(GURL("http://asdfasdf.com/"),
+ &consumer_,
+ NewCallback(static_cast<HistoryTest*>(this),
+ &HistoryTest::OnThumbnailDataAvailable)));
+ thumbnail_data_.clear();
+ MessageLoop::current()->Run();
+ EXPECT_EQ(0, thumbnail_data_.size());
+
+ // Request the thumbnail and cancel the request..
+ got_thumbnail_callback_ = false;
+ thumbnail_data_.clear();
+ HistoryService::Handle handle = history->GetPageThumbnail(url, &consumer_,
+ NewCallback(static_cast<HistoryTest*>(this),
+ &HistoryTest::OnThumbnailDataAvailable));
+ EXPECT_TRUE(handle);
+
+ history->CancelRequest(handle);
+
+ // We create a task with a timeout so we can make sure we don't get and
+ // data in that time.
+ class QuitMessageLoop : public Task {
+ public:
+ virtual void Run() {
+ MessageLoop::current()->Quit();
+ }
+ };
+ MessageLoop::current()->PostDelayedTask(FROM_HERE, new QuitMessageLoop, 2000);
+ MessageLoop::current()->Run();
+ EXPECT_FALSE(got_thumbnail_callback_);
+}
+
+// The version of the history database should be current in the "typical
+// history" example file or it will be imported on startup, throwing off timing
+// measurements.
+//
+// See test/data/profiles/typical_history/README.txt for instructions on
+// how to up the version.
+TEST(HistoryProfileTest, TypicalProfileVersion) {
+ std::wstring file;
+ ASSERT_TRUE(PathService::Get(chrome::DIR_TEST_DATA, &file));
+ file_util::AppendToPath(&file, L"profiles");
+ file_util::AppendToPath(&file, L"typical_history");
+ file_util::AppendToPath(&file, L"Default");
+ file_util::AppendToPath(&file, L"History");
+
+ int cur_version = HistoryDatabase::GetCurrentVersion();
+
+ sqlite3* db;
+ ASSERT_EQ(SQLITE_OK, sqlite3_open(WideToUTF8(file).c_str(), &db));
+
+ SQLStatement s;
+ ASSERT_EQ(SQLITE_OK, s.prepare(db,
+ "SELECT value FROM meta WHERE key = 'version'"));
+ EXPECT_EQ(SQLITE_ROW, s.step());
+ int file_version = s.column_int(0);
+
+ sqlite3_close(db);
+
+ EXPECT_EQ(cur_version, file_version);
+}
+
+namespace {
+
+// Use this dummy value to scope the page IDs we give history.
+static const void* kAddArgsScope = (void*)0x12345678;
+
+// Creates a new HistoryAddPageArgs object for sending to the history database
+// with reasonable defaults and the given NULL-terminated URL string. The
+// returned object will NOT be add-ref'ed, which is the responsibility of the
+// caller.
+HistoryAddPageArgs* MakeAddArgs(const GURL& url) {
+ return new HistoryAddPageArgs(url,
+ Time::Now(),
+ kAddArgsScope,
+ 0,
+ GURL(),
+ HistoryService::RedirectList(),
+ PageTransition::TYPED);
+}
+
+// Convenience version of the above to convert a char string.
+HistoryAddPageArgs* MakeAddArgs(const char* url) {
+ return MakeAddArgs(GURL(url));
+}
+
+} // namespace
+
+namespace {
+
+// A HistoryDBTask implementation. Each time RunOnDBThread is invoked
+// invoke_count is increment. When invoked kWantInvokeCount times, true is
+// returned from RunOnDBThread which should stop RunOnDBThread from being
+// invoked again. When DoneRunOnMainThread is invoked, done_invoked is set to
+// true.
+class HistoryDBTaskImpl : public HistoryDBTask {
+ public:
+ static const int kWantInvokeCount = 2;
+
+ HistoryDBTaskImpl() : invoke_count(0), done_invoked(false) {}
+ virtual ~HistoryDBTaskImpl() {}
+
+ virtual bool RunOnDBThread(HistoryBackend* backend, HistoryDatabase* db) {
+ return (++invoke_count == kWantInvokeCount);
+ }
+
+ virtual void DoneRunOnMainThread() {
+ done_invoked = true;
+ MessageLoop::current()->Quit();
+ }
+
+ int invoke_count;
+ bool done_invoked;
+
+ private:
+ DISALLOW_EVIL_CONSTRUCTORS(HistoryDBTaskImpl);
+};
+
+} // namespace
+
+TEST_F(HistoryTest, HistoryDBTask) {
+ CancelableRequestConsumerT<int, 0> request_consumer;
+ HistoryService* history = new HistoryService();
+ ASSERT_TRUE(history->Init(history_dir_));
+ scoped_refptr<HistoryDBTaskImpl> task(new HistoryDBTaskImpl());
+ history_service_ = history;
+ history->ScheduleDBTask(task.get(), &request_consumer);
+ // Run the message loop. When HistoryDBTaskImpl::DoneRunOnMainThread runs,
+ // it will stop the message loop. If the test hangs here, it means
+ // DoneRunOnMainThread isn't being invoked correctly.
+ MessageLoop::current()->Run();
+ CleanupHistoryService();
+ // WARNING: history has now been deleted.
+ history = NULL;
+ ASSERT_EQ(HistoryDBTaskImpl::kWantInvokeCount, task->invoke_count);
+ ASSERT_TRUE(task->done_invoked);
+}
+
+TEST_F(HistoryTest, HistoryDBTaskCanceled) {
+ CancelableRequestConsumerT<int, 0> request_consumer;
+ HistoryService* history = new HistoryService();
+ ASSERT_TRUE(history->Init(history_dir_));
+ scoped_refptr<HistoryDBTaskImpl> task(new HistoryDBTaskImpl());
+ history_service_ = history;
+ history->ScheduleDBTask(task.get(), &request_consumer);
+ request_consumer.CancelAllRequests();
+ CleanupHistoryService();
+ // WARNING: history has now been deleted.
+ history = NULL;
+ ASSERT_FALSE(task->done_invoked);
+}
+
+TEST_F(HistoryTest, Starring) {
+ CreateBackendAndDatabase();
+
+ // Add one page and star it.
+ GURL simple_page("http://google.com");
+ scoped_refptr<HistoryAddPageArgs> args(MakeAddArgs(simple_page));
+ backend_->AddPage(args);
+ EXPECT_FALSE(IsURLStarred(db_, simple_page));
+ EXPECT_FALSE(IsURLStarred(in_mem_backend_->db(), simple_page));
+ SetURLStarred(simple_page, true);
+
+ // The URL should be starred in both the main and memory DBs.
+ EXPECT_TRUE(IsURLStarred(db_, simple_page));
+ EXPECT_TRUE(IsURLStarred(in_mem_backend_->db(), simple_page));
+
+ // Unstar it.
+ SetURLStarred(simple_page, false);
+ EXPECT_FALSE(IsURLStarred(db_, simple_page));
+ EXPECT_FALSE(IsURLStarred(in_mem_backend_->db(), simple_page));
+}
+
+TEST_F(HistoryTest, SetStarredOnPageWithTypeCount0) {
+ CreateBackendAndDatabase();
+
+ // Add a page to the backend.
+ const GURL url(L"http://google.com/");
+ scoped_refptr<HistoryAddPageArgs> args(new HistoryAddPageArgs(
+ url, Time::Now(), NULL, 1, GURL(), HistoryService::RedirectList(),
+ PageTransition::LINK));
+ backend_->AddPage(args);
+
+ // Now fetch the URLInfo from the in memory db, it should not be there since
+ // it was not typed.
+ URLRow url_info;
+ EXPECT_EQ(0, in_mem_backend_->db()->GetRowForURL(url, &url_info));
+
+ // Mark the URL starred.
+ SetURLStarred(url, true);
+
+ // The type count is 0, so the page shouldn't be starred in the in memory
+ // db.
+ EXPECT_EQ(0, in_mem_backend_->db()->GetRowForURL(url, &url_info));
+ EXPECT_TRUE(IsURLStarred(db_, url));
+
+ // Now unstar it.
+ SetURLStarred(url, false);
+
+ // Make sure both the back end and in memory DB think it is unstarred.
+ EXPECT_EQ(0, in_mem_backend_->db()->GetRowForURL(url, &url_info));
+ EXPECT_FALSE(IsURLStarred(db_, url));
+}
+
+} // namespace history
diff --git a/chrome/browser/history/in_memory_database.cc b/chrome/browser/history/in_memory_database.cc
new file mode 100644
index 0000000..f4a801a
--- /dev/null
+++ b/chrome/browser/history/in_memory_database.cc
@@ -0,0 +1,127 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/in_memory_database.h"
+
+#include "base/logging.h"
+#include "base/string_util.h"
+
+namespace history {
+
+InMemoryDatabase::InMemoryDatabase() : URLDatabase(), db_(NULL) {
+}
+
+InMemoryDatabase::~InMemoryDatabase() {
+}
+
+bool InMemoryDatabase::InitDB() {
+ DCHECK(!db_) << "Already initialized!";
+ if (sqlite3_open(":memory:", &db_) != SQLITE_OK) {
+ NOTREACHED() << "Cannot open memory database";
+ return false;
+ }
+ statement_cache_ = new SqliteStatementCache(db_);
+ DBCloseScoper scoper(&db_, &statement_cache_); // closes the DB on error
+
+ // No reason to leave data behind in memory when rows are removed.
+ sqlite3_exec(db_, "PRAGMA auto_vacuum=1", NULL, NULL, NULL);
+ // Set the database page size to 4K for better performance.
+ sqlite3_exec(db_, "PRAGMA page_size=4096", NULL, NULL, NULL);
+ // Ensure this is really an in-memory-only cache.
+ sqlite3_exec(db_, "PRAGMA temp_store=MEMORY", NULL, NULL, NULL);
+
+ // Create the URL table, but leave it empty for now.
+ if (!CreateURLTable(false)) {
+ NOTREACHED() << "Unable to create table";
+ return false;
+ }
+
+ // Succeeded, keep the DB open.
+ scoper.Detach();
+ db_closer_.Attach(&db_, &statement_cache_);
+ return true;
+}
+
+bool InMemoryDatabase::InitFromScratch() {
+ if (!InitDB())
+ return false;
+
+ // InitDB doesn't create the index so in the disk-loading case, it can be
+ // added afterwards.
+ CreateMainURLIndex();
+ return true;
+}
+
+bool InMemoryDatabase::InitFromDisk(const std::wstring& history_name) {
+ if (!InitDB())
+ return false;
+
+ // Attach to the history database on disk. (We can't ATTACH in the middle of
+ // a transaction.)
+ SQLStatement attach;
+ if (attach.prepare(db_, "ATTACH ? AS history") != SQLITE_OK) {
+ NOTREACHED() << "Unable to attach to history database.";
+ return false;
+ }
+ attach.bind_string(0, WideToUTF8(history_name));
+ if (attach.step() != SQLITE_DONE) {
+ NOTREACHED() << "Unable to bind";
+ return false;
+ }
+
+ // Copy URL data to memory.
+ if (sqlite3_exec(db_,
+ "INSERT INTO urls SELECT * FROM history.urls WHERE typed_count > 0",
+ NULL, NULL, NULL) != SQLITE_OK) {
+ // Unable to get data from the history database. This is OK, the file may
+ // just not exist yet.
+ }
+
+ // Detach from the history database on disk.
+ if (sqlite3_exec(db_, "DETACH history", NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED() << "Unable to detach from history database.";
+ return false;
+ }
+
+ // Index the table, this is faster than creating the index first and then
+ // inserting into it.
+ CreateMainURLIndex();
+
+ return true;
+}
+
+sqlite3* InMemoryDatabase::GetDB() {
+ return db_;
+}
+
+SqliteStatementCache& InMemoryDatabase::GetStatementCache() {
+ return *statement_cache_;
+}
+
+} // namespace history
diff --git a/chrome/browser/history/in_memory_database.h b/chrome/browser/history/in_memory_database.h
new file mode 100644
index 0000000..a3b9bad
--- /dev/null
+++ b/chrome/browser/history/in_memory_database.h
@@ -0,0 +1,79 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_HISTORY_MEMORY_DB_H__
+#define CHROME_BROWSER_HISTORY_HISTORY_MEMORY_DB_H__
+
+#include "base/basictypes.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/common/sqlite_compiled_statement.h"
+
+struct sqlite3;
+
+namespace history {
+
+// Class used for a fast in-memory cache of typed URLs. Used for inline
+// autocomplete since it is fast enough to be called synchronously as the user
+// is typing.
+class InMemoryDatabase : public URLDatabase {
+ public:
+ InMemoryDatabase();
+ virtual ~InMemoryDatabase();
+
+ // Creates an empty in-memory database.
+ bool InitFromScratch();
+
+ // Initializes the database by directly slurping the data from the given
+ // file. Conceptually, the InMemoryHistoryBackend should do the populating
+ // after this object does some common initialization, but that would be
+ // much slower.
+ bool InitFromDisk(const std::wstring& history_name);
+
+ protected:
+ // Implemented for URLDatabase.
+ virtual sqlite3* GetDB();
+ virtual SqliteStatementCache& GetStatementCache();
+
+ private:
+ // Initializes the database connection, this is the shared code between
+ // InitFromScratch() and InitFromDisk() above. Returns true on success.
+ bool InitDB();
+
+ // The close scoper will free the database and delete the statement cache in
+ // the correct order automatically when we are destroyed.
+ DBCloseScoper db_closer_;
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(InMemoryDatabase);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_HISTORY_MEMORY_DB_H__
diff --git a/chrome/browser/history/in_memory_history_backend.cc b/chrome/browser/history/in_memory_history_backend.cc
new file mode 100644
index 0000000..b3619a9
--- /dev/null
+++ b/chrome/browser/history/in_memory_history_backend.cc
@@ -0,0 +1,184 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/in_memory_history_backend.h"
+
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/history/history_database.h"
+#include "chrome/browser/history/history_notifications.h"
+#include "chrome/browser/history/in_memory_database.h"
+#include "chrome/browser/profile.h"
+
+namespace history {
+
+// If a page becomes starred we use this id in place of the real starred id.
+// See note in OnURLsStarred.
+static const StarID kBogusStarredID = 0x0FFFFFFF;
+
+InMemoryHistoryBackend::InMemoryHistoryBackend()
+ : profile_(NULL),
+ registered_for_notifications_(false) {
+}
+
+InMemoryHistoryBackend::~InMemoryHistoryBackend() {
+ if (registered_for_notifications_) {
+ NotificationService* service = NotificationService::current();
+
+ Source<Profile> source(profile_);
+ service->RemoveObserver(this, NOTIFY_HISTORY_URL_VISITED, source);
+ service->RemoveObserver(this, NOTIFY_HISTORY_TYPED_URLS_MODIFIED, source);
+ service->RemoveObserver(this, NOTIFY_HISTORY_URLS_DELETED, source);
+ service->RemoveObserver(this, NOTIFY_URLS_STARRED, source);
+ }
+}
+
+bool InMemoryHistoryBackend::Init(const std::wstring& history_filename) {
+ db_.reset(new InMemoryDatabase);
+ return db_->InitFromDisk(history_filename);
+}
+
+void InMemoryHistoryBackend::AttachToHistoryService(Profile* profile) {
+ if (!db_.get()) {
+ NOTREACHED();
+ return;
+ }
+
+ // We only want notifications for the associated profile.
+ profile_ = profile;
+ Source<Profile> source(profile_);
+
+ // TODO(evanm): this is currently necessitated by generate_profile, which
+ // runs without a browser process. generate_profile should really create
+ // a browser process, at which point this check can then be nuked.
+ if (!g_browser_process)
+ return;
+
+ // Register for the notifications we care about.
+ // TODO(tc): Make a ScopedNotificationObserver so we don't have to remember
+ // to remove these manually.
+ registered_for_notifications_ = true;
+ NotificationService* service = NotificationService::current();
+ service->AddObserver(this, NOTIFY_HISTORY_URL_VISITED, source);
+ service->AddObserver(this, NOTIFY_HISTORY_TYPED_URLS_MODIFIED, source);
+ service->AddObserver(this, NOTIFY_HISTORY_URLS_DELETED, source);
+ service->AddObserver(this, NOTIFY_URLS_STARRED, source);
+}
+
+void InMemoryHistoryBackend::Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type) {
+ case NOTIFY_HISTORY_URL_VISITED: {
+ Details<history::URLVisitedDetails> visited_details(details);
+ if (visited_details->row.typed_count() > 0) {
+ URLsModifiedDetails modified_details;
+ modified_details.changed_urls.push_back(visited_details->row);
+ OnTypedURLsModified(modified_details);
+ }
+ break;
+ }
+ case NOTIFY_HISTORY_TYPED_URLS_MODIFIED:
+ OnTypedURLsModified(
+ *Details<history::URLsModifiedDetails>(details).ptr());
+ break;
+ case NOTIFY_HISTORY_URLS_DELETED:
+ OnURLsDeleted(*Details<history::URLsDeletedDetails>(details).ptr());
+ break;
+ case NOTIFY_URLS_STARRED:
+ OnURLsStarred(*Details<history::URLsStarredDetails>(details).ptr());
+ break;
+ default:
+ // For simplicity, the unit tests send us all notifications, even when
+ // we haven't registered for them, so don't assert here.
+ break;
+ }
+}
+
+void InMemoryHistoryBackend::OnTypedURLsModified(
+ const URLsModifiedDetails& details) {
+ DCHECK(db_.get());
+
+ // Add or update the URLs.
+ //
+ // TODO(brettw) currently the rows in the in-memory database don't match the
+ // IDs in the main database. This sucks. Instead of Add and Remove, we should
+ // have Sync(), which would take the ID if it's given and add it.
+ std::vector<history::URLRow>::const_iterator i;
+ for (i = details.changed_urls.begin();
+ i != details.changed_urls.end(); i++) {
+ URLID id = db_->GetRowForURL(i->url(), NULL);
+ if (id)
+ db_->UpdateURLRow(id, *i);
+ else
+ db_->AddURL(*i);
+ }
+}
+
+void InMemoryHistoryBackend::OnURLsDeleted(const URLsDeletedDetails& details) {
+ DCHECK(db_.get());
+
+ if (details.all_history) {
+ // When all history is deleted, the individual URLs won't be listed. Just
+ // create a new database to quickly clear everything out.
+ db_.reset(new InMemoryDatabase);
+ if (!db_->InitFromScratch())
+ db_.reset();
+ return;
+ }
+
+ // Delete all matching URLs in our database.
+ for (std::set<GURL>::const_iterator i = details.urls.begin();
+ i != details.urls.end(); ++i) {
+ URLID id = db_->GetRowForURL(*i, NULL);
+ if (id) {
+ // We typically won't have most of them since we only have a subset of
+ // history, so ignore errors.
+ db_->DeleteURLRow(id);
+ }
+ }
+}
+
+void InMemoryHistoryBackend::OnURLsStarred(
+ const history::URLsStarredDetails& details) {
+ DCHECK(db_.get());
+
+ for (std::set<GURL>::const_iterator i = details.changed_urls.begin();
+ i != details.changed_urls.end(); ++i) {
+ URLRow row;
+ if (db_->GetRowForURL(*i, &row)) {
+ // NOTE: We currently don't care about the star id from the in memory
+ // db, so that we use a fake value. If this does become a problem,
+ // then the notification will have to propagate the star id.
+ row.set_star_id(details.starred ? kBogusStarredID : 0);
+ db_->UpdateURLRow(row.id(), row);
+ }
+ }
+}
+
+} // namespace history
diff --git a/chrome/browser/history/in_memory_history_backend.h b/chrome/browser/history/in_memory_history_backend.h
new file mode 100644
index 0000000..9134bf0
--- /dev/null
+++ b/chrome/browser/history/in_memory_history_backend.h
@@ -0,0 +1,107 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Contains the history backend wrapper around the in-memory URL database. This
+// object maintains an in-memory cache of the subset of history required to do
+// in-line autocomplete.
+//
+// It is created on the history thread and passed to the main thread where
+// operations can be completed synchronously. It listenes for notifications
+// from the "regular" history backend and keeps itself in sync.
+
+#ifndef CHROME_BROWSER_HISTORY_IN_MEMORY_HISTORY_BACKEND_H__
+#define CHROME_BROWSER_HISTORY_IN_MEMORY_HISTORY_BACKEND_H__
+
+#include <string>
+
+#include "base/basictypes.h"
+#include "chrome/browser/history/history_notifications.h"
+#include "chrome/common/notification_service.h"
+
+class HistoryDatabase;
+class Profile;
+
+namespace history {
+
+class InMemoryDatabase;
+
+class InMemoryHistoryBackend : public NotificationObserver {
+ public:
+ InMemoryHistoryBackend();
+ ~InMemoryHistoryBackend();
+
+ // Initializes with data from the given history database.
+ bool Init(const std::wstring& history_filename);
+
+ // Does initialization work when this object is attached to the history
+ // system on the main thread. The argument is the profile with which the
+ // attached history service is under.
+ void AttachToHistoryService(Profile* profile);
+
+ // Returns the underlying database associated with this backend. The current
+ // autocomplete code was written fro this, but it should probably be removed
+ // so that it can deal directly with this object, rather than the DB.
+ InMemoryDatabase* db() const {
+ return db_.get();
+ }
+
+ // Notification callback.
+ virtual void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details);
+
+ private:
+ FRIEND_TEST(HistoryBackendTest, DeleteAll);
+
+ // Handler for NOTIFY_HISTORY_TYPED_URLS_MODIFIED.
+ void OnTypedURLsModified(const URLsModifiedDetails& details);
+
+ // Handler for NOTIFY_HISTORY_URLS_DELETED.
+ void OnURLsDeleted(const URLsDeletedDetails& details);
+
+ // Handler for NOTIFY_URLS_STARRED.
+ void OnURLsStarred(const URLsStarredDetails& details);
+
+ scoped_ptr<InMemoryDatabase> db_;
+
+ // The profile that this object is attached. May be NULL before
+ // initialization.
+ Profile* profile_;
+
+ // Set when this object has registered for notifications. This is done so we
+ // know whether to unregister (the initialization may have failed, so we
+ // may be destroyed before attaching to the main thread.
+ bool registered_for_notifications_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(InMemoryHistoryBackend);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_IN_MEMORY_HISTORY_BACKEND_H__
diff --git a/chrome/browser/history/page_usage_data.cc b/chrome/browser/history/page_usage_data.cc
new file mode 100644
index 0000000..9716311
--- /dev/null
+++ b/chrome/browser/history/page_usage_data.cc
@@ -0,0 +1,38 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <algorithm>
+
+#include "chrome/browser/history/page_usage_data.h"
+
+//static
+bool PageUsageData::Predicate(const PageUsageData* lhs,
+ const PageUsageData* rhs) {
+ return lhs->GetScore() > rhs->GetScore();
+}
diff --git a/chrome/browser/history/page_usage_data.h b/chrome/browser/history/page_usage_data.h
new file mode 100644
index 0000000..abe8c2c
--- /dev/null
+++ b/chrome/browser/history/page_usage_data.h
@@ -0,0 +1,173 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_PAGE_USAGE_DATA_H__
+#define CHROME_BROWSER_HISTORY_PAGE_USAGE_DATA_H__
+
+#include "SkBitmap.h"
+
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/history_types.h"
+#include "googleurl/src/gurl.h"
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// PageUsageData
+//
+// A per domain usage data structure to compute and manage most visited
+// pages.
+//
+// See History::QueryPageUsageSince()
+//
+/////////////////////////////////////////////////////////////////////////////
+class PageUsageData {
+ public:
+ PageUsageData(history::URLID id)
+ : id_(id),
+ thumbnail_(NULL),
+ thumbnail_set_(false),
+ thumbnail_pending_(false),
+ favicon_(NULL),
+ favicon_set_(false),
+ favicon_pending_(false),
+ score_(0.0) {
+ }
+
+ virtual ~PageUsageData() {
+ delete thumbnail_;
+ delete favicon_;
+ }
+
+ // Return the url ID
+ history::URLID GetID() const {
+ return id_;
+ }
+
+ void SetURL(const GURL& url) {
+ url_ = url;
+ }
+
+ const GURL& GetURL() const {
+ return url_;
+ }
+
+ void SetTitle(const std::wstring& s) {
+ title_ = s;
+ }
+
+ const std::wstring& GetTitle() const {
+ return title_;
+ }
+
+ void SetScore(double v) {
+ score_ = v;
+ }
+
+ double GetScore() const {
+ return score_;
+ }
+
+ void SetThumbnailMissing() {
+ thumbnail_set_ = true;
+ }
+
+ void SetThumbnail(SkBitmap* img) {
+ if (thumbnail_ && thumbnail_ != img)
+ delete thumbnail_;
+
+ thumbnail_ = img;
+ thumbnail_set_ = true;
+ }
+
+ bool HasThumbnail() const {
+ return thumbnail_set_;
+ }
+
+ const SkBitmap* GetThumbnail() const {
+ return thumbnail_;
+ }
+
+ bool thumbnail_pending() const {
+ return thumbnail_pending_;
+ }
+
+ void set_thumbnail_pending(bool pending) {
+ thumbnail_pending_ = pending;
+ }
+
+ void SetFavIconMissing() {
+ favicon_set_ = true;
+ }
+
+ void SetFavIcon(SkBitmap* img) {
+ if (favicon_ && favicon_ != img)
+ delete favicon_;
+ favicon_ = img;
+ favicon_set_ = true;
+ }
+
+ bool HasFavIcon() const {
+ return favicon_set_;
+ }
+
+ bool favicon_pending() const {
+ return favicon_pending_;
+ }
+
+ void set_favicon_pending(bool pending) {
+ favicon_pending_ = pending;
+ }
+
+ const SkBitmap* GetFavIcon() const {
+ return favicon_;
+ }
+
+ // Sort predicate to sort instances by score (high to low)
+ static bool Predicate(const PageUsageData* dud1,
+ const PageUsageData* dud2);
+
+ private:
+ history::URLID id_;
+ GURL url_;
+ std::wstring title_;
+
+ SkBitmap* thumbnail_;
+ bool thumbnail_set_;
+ // Whether we have an outstanding request for the thumbnail.
+ bool thumbnail_pending_;
+
+ SkBitmap* favicon_;
+ bool favicon_set_;
+ // Whether we have an outstanding request for the favicon.
+ bool favicon_pending_;
+
+ double score_;
+};
+
+#endif // CHROME_BROWSER_HISTORY_PAGE_USAGE_DATA_H__
diff --git a/chrome/browser/history/query_parser.cc b/chrome/browser/history/query_parser.cc
new file mode 100644
index 0000000..86285f3
--- /dev/null
+++ b/chrome/browser/history/query_parser.cc
@@ -0,0 +1,317 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/query_parser.h"
+
+#include "base/logging.h"
+#include "base/string_util.h"
+#include "base/word_iterator.h"
+#include "chrome/common/l10n_util.h"
+#include "chrome/common/scoped_vector.h"
+#include "unicode/uscript.h"
+
+// For CJK ideographs and Korean Hangul, even a single character
+// can be useful in prefix matching, but that may give us too many
+// false positives. Moreover, the current ICU word breaker gives us
+// back every single Chinese character as a word so that there's no
+// point doing anything for them and we only adjust the minimum length
+// to 2 for Korean Hangul while using 3 for others. This is a temporary
+// hack until we have a segmentation support.
+static inline bool IsWordLongEnoughForPrefixSearch(const std::wstring& word)
+{
+ DCHECK(word.size() > 0);
+ size_t minimum_length = 3;
+ // We intentionally exclude Hangul Jamos (both Conjoining and compatibility)
+ // because they 'behave like' Latin letters. Moreover, we should
+ // normalize the former before reaching here.
+ if (0xAC00 <= word[0] && word[0] <= 0xD7A3)
+ minimum_length = 2;
+ return word.size() >= minimum_length;
+}
+
+// Inheritance structure:
+// Queries are represented as trees of QueryNodes.
+// QueryNodes are either a collection of subnodes (a QueryNodeList)
+// or a single word (a QueryNodeWord).
+
+// A QueryNodeWord is a single word in the query.
+class QueryNodeWord : public QueryNode {
+ public:
+ QueryNodeWord(const std::wstring& word) : word_(word), literal_(false) {}
+ virtual ~QueryNodeWord() {}
+ virtual int AppendToSQLiteQuery(std::wstring* query) const;
+ virtual bool IsWord() const { return true; }
+
+ const std::wstring& word() const { return word_; }
+ void set_literal(bool literal) { literal_ = literal; }
+
+ virtual bool HasMatchIn(const std::vector<std::wstring>& words) const;
+
+ virtual bool Matches(const std::wstring& word, bool exact) const;
+
+ private:
+ std::wstring word_;
+ bool literal_;
+};
+
+bool QueryNodeWord::HasMatchIn(const std::vector<std::wstring>& words) const {
+ for (size_t i = 0; i < words.size(); ++i) {
+ if (Matches(words[i], false))
+ return true;
+ }
+ return false;
+}
+
+bool QueryNodeWord::Matches(const std::wstring& word, bool exact) const {
+ if (exact || !IsWordLongEnoughForPrefixSearch(word_))
+ return word == word_;
+ return word.size() >= word_.size() &&
+ (word_.compare(0, word_.size(), word, 0, word_.size()) == 0);
+}
+
+int QueryNodeWord::AppendToSQLiteQuery(std::wstring* query) const {
+ query->append(word_);
+
+ // Use prefix search if we're not literal and long enough.
+ if (!literal_ && IsWordLongEnoughForPrefixSearch(word_))
+ *query += L'*';
+ return 1;
+}
+
+// A QueryNodeList has a collection of child QueryNodes
+// which it cleans up after.
+class QueryNodeList : public QueryNode {
+ public:
+ virtual ~QueryNodeList();
+
+ virtual int AppendToSQLiteQuery(std::wstring* query) const {
+ return AppendChildrenToString(query);
+ }
+ virtual bool IsWord() const { return false; }
+
+ void AddChild(QueryNode* node) { children_.push_back(node); }
+
+ typedef std::vector<QueryNode*> QueryNodeVector;
+ QueryNodeVector* children() { return &children_; }
+
+ // Remove empty subnodes left over from other parsing.
+ void RemoveEmptySubnodes();
+
+ // QueryNodeList is never used with Matches or HasMatchIn.
+ virtual bool Matches(const std::wstring& word, bool exact) const {
+ NOTREACHED();
+ return false;
+ }
+ virtual bool HasMatchIn(const std::vector<std::wstring>& words) const {
+ NOTREACHED();
+ return false;
+ }
+
+ protected:
+ int AppendChildrenToString(std::wstring* query) const;
+
+ QueryNodeVector children_;
+};
+
+QueryNodeList::~QueryNodeList() {
+ for (QueryNodeVector::iterator node = children_.begin();
+ node != children_.end(); ++node)
+ delete *node;
+}
+
+int QueryNodeList::AppendChildrenToString(std::wstring* query) const {
+ int num_words = 0;
+ for (QueryNodeVector::const_iterator node = children_.begin();
+ node != children_.end(); ++node) {
+ if (node != children_.begin())
+ query->push_back(L' ');
+ num_words += (*node)->AppendToSQLiteQuery(query);
+ }
+ return num_words;
+}
+
+// A QueryNodePhrase is a phrase query ("quoted").
+class QueryNodePhrase : public QueryNodeList {
+ public:
+ virtual int AppendToSQLiteQuery(std::wstring* query) const {
+ query->push_back(L'"');
+ int num_words = AppendChildrenToString(query);
+ query->push_back(L'"');
+ return num_words;
+ }
+
+ virtual bool Matches(const std::wstring& word, bool exact) const;
+ virtual bool HasMatchIn(const std::vector<std::wstring>& words) const;
+};
+
+bool QueryNodePhrase::Matches(const std::wstring& word, bool exact) const {
+ NOTREACHED();
+ return false;
+}
+
+bool QueryNodePhrase::HasMatchIn(const std::vector<std::wstring>& words) const {
+ if (words.size() < children_.size())
+ return false;
+
+ for (size_t i = 0, max = words.size() - children_.size() + 1; i < max; ++i) {
+ bool matched_all = true;
+ for (size_t j = 0; j < children_.size(); ++j) {
+ if (!children_[j]->Matches(words[i + j], true)) {
+ matched_all = false;
+ break;
+ }
+ }
+ if (matched_all)
+ return true;
+ }
+ return false;
+}
+
+QueryParser::QueryParser() {
+}
+
+// Returns true if the character is considered a quote.
+static bool IsQueryQuote(wchar_t ch) {
+ return ch == '"' ||
+ ch == 0xab || // left pointing double angle bracket
+ ch == 0xbb || // right pointing double angle bracket
+ ch == 0x201c || // left double quotation mark
+ ch == 0x201d || // right double quotation mark
+ ch == 0x201e; // double low-9 quotation mark
+}
+
+int QueryParser::ParseQuery(const std::wstring& query,
+ std::wstring* sqlite_query) {
+ QueryNodeList root;
+ if (!ParseQueryImpl(query, &root))
+ return 0;
+ return root.AppendToSQLiteQuery(sqlite_query);
+}
+
+void QueryParser::ParseQuery(const std::wstring& query,
+ std::vector<QueryNode*>* nodes) {
+ QueryNodeList root;
+ if (ParseQueryImpl(l10n_util::ToLower(query), &root))
+ nodes->swap(*root.children());
+}
+
+bool QueryParser::DoesQueryMatch(const std::wstring& text,
+ const std::vector<QueryNode*>& query_nodes) {
+ if (query_nodes.empty())
+ return false;
+
+ std::vector<std::wstring> query_words;
+ ExtractWords(l10n_util::ToLower(text), &query_words);
+
+ if (query_words.empty())
+ return false;
+
+ for (size_t i = 0; i < query_nodes.size(); ++i) {
+ if (!query_nodes[i]->HasMatchIn(query_words))
+ return false;
+ }
+ return true;
+}
+
+bool QueryParser::ParseQueryImpl(const std::wstring& query,
+ QueryNodeList* root) {
+ WordIterator iter(query, WordIterator::BREAK_WORD);
+ // TODO(evanm): support a locale here
+ if (!iter.Init())
+ return false;
+
+ // To handle nesting, we maintain a stack of QueryNodeLists.
+ // The last element (back) of the stack contains the current, deepest node.
+ std::vector<QueryNodeList*> query_stack;
+ query_stack.push_back(root);
+
+ bool in_quotes = false; // whether we're currently in a quoted phrase
+ while (iter.Advance()) {
+ // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It
+ // is not necessarily a word, but could also be a sequence of punctuation
+ // or whitespace.
+ if (iter.IsWord()) {
+ std::wstring word = iter.GetWord();
+
+ QueryNodeWord* word_node = new QueryNodeWord(word);
+ if (in_quotes)
+ word_node->set_literal(true);
+ query_stack.back()->AddChild(word_node);
+ } else { // Punctuation.
+ if (IsQueryQuote(query[iter.prev()])) {
+ if (!in_quotes) {
+ QueryNodeList* quotes_node = new QueryNodePhrase;
+ query_stack.back()->AddChild(quotes_node);
+ query_stack.push_back(quotes_node);
+ in_quotes = true;
+ } else {
+ query_stack.pop_back(); // stop adding to the quoted phrase
+ in_quotes = false;
+ }
+ }
+ }
+ }
+
+ root->RemoveEmptySubnodes();
+ return true;
+}
+
+void QueryParser::ExtractWords(const std::wstring& text,
+ std::vector<std::wstring>* words) {
+ WordIterator iter(text, WordIterator::BREAK_WORD);
+ // TODO(evanm): support a locale here
+ if (!iter.Init())
+ return;
+
+ while (iter.Advance()) {
+ // Just found a span between 'prev' (inclusive) and 'pos' (exclusive). It
+ // is not necessarily a word, but could also be a sequence of punctuation
+ // or whitespace.
+ if (iter.IsWord()) {
+ std::wstring word = iter.GetWord();
+ if (!word.empty())
+ words->push_back(word);
+ }
+ }
+}
+
+void QueryNodeList::RemoveEmptySubnodes() {
+ for (size_t i = 0; i < children_.size(); ++i) {
+ if (children_[i]->IsWord())
+ continue;
+
+ QueryNodeList* list_node = static_cast<QueryNodeList*>(children_[i]);
+ list_node->RemoveEmptySubnodes();
+ if (list_node->children()->empty()) {
+ children_.erase(children_.begin() + i);
+ --i;
+ delete list_node;
+ }
+ }
+}
diff --git a/chrome/browser/history/query_parser.h b/chrome/browser/history/query_parser.h
new file mode 100644
index 0000000..78ab257
--- /dev/null
+++ b/chrome/browser/history/query_parser.h
@@ -0,0 +1,97 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// The query parser is used to parse queries entered into the history
+// search into more normalized queries can be passed to the SQLite backend.
+
+#ifndef CHROME_BROWSER_HISTORY_QUERY_PARSER_H__
+#define CHROME_BROWSER_HISTORY_QUERY_PARSER_H__
+
+#include <set>
+#include <vector>
+
+class QueryNodeList;
+
+// QueryNode is used by QueryNodeParser to represent the elements that
+// constitute a query. While QueryNode is exposed by way of ParseQuery, it
+// really isn't meant for external usage.
+class QueryNode {
+ public:
+ virtual ~QueryNode() {}
+
+ // Serialize ourselves out to a string that can be passed to SQLite. Returns
+ // the number of words in this node.
+ virtual int AppendToSQLiteQuery(std::wstring* query) const = 0;
+
+ // Return true if this is a word node, false if it's a QueryNodeList.
+ virtual bool IsWord() const = 0;
+
+ // Returns true if this node matches the specified text. If exact is true,
+ // the string must exactly match. Otherwise, this uses a starts with
+ // comparison.
+ virtual bool Matches(const std::wstring& word, bool exact) const = 0;
+
+ // Returns true if this node matches at least one of the words in words.
+ virtual bool HasMatchIn(const std::vector<std::wstring>& words) const = 0;
+};
+
+
+class QueryParser {
+ public:
+ QueryParser();
+
+ // Parse a query into a SQLite query. The resulting query is placed in
+ // sqlite_query and the number of words is returned.
+ int ParseQuery(const std::wstring& query,
+ std::wstring* sqlite_query);
+
+ // Parses the query words in query, returning the nodes that constitute the
+ // valid words in the query. This is intended for later usage with
+ // DoesQueryMatch.
+ // Ownership of the nodes passes to the caller.
+ void ParseQuery(const std::wstring& query,
+ std::vector<QueryNode*>* nodes);
+
+ // Returns true if the string text matches the query nodes created by a call
+ // to ParseQuery.
+ bool DoesQueryMatch(const std::wstring& text,
+ const std::vector<QueryNode*>& nodes);
+
+ private:
+ // Does the work of parsing a query; creates nodes in QueryNodeList as
+ // appropriate. This is invoked from both of the ParseQuery methods.
+ bool ParseQueryImpl(const std::wstring& query,
+ QueryNodeList* root);
+
+ // Extracts the words from text, placing each word into words.
+ void ExtractWords(const std::wstring& text,
+ std::vector<std::wstring>* words);
+};
+
+#endif // CHROME_BROWSER_HISTORY_QUERY_PARSER_H__
diff --git a/chrome/browser/history/query_parser_unittest.cc b/chrome/browser/history/query_parser_unittest.cc
new file mode 100644
index 0000000..f76f196
--- /dev/null
+++ b/chrome/browser/history/query_parser_unittest.cc
@@ -0,0 +1,137 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/query_parser.h"
+#include "base/logging.h"
+#include "chrome/common/scoped_vector.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class QueryParserTest : public testing::Test {
+ public:
+ struct TestData {
+ const std::wstring input;
+ const int expected_word_count;
+ };
+
+ std::wstring QueryToString(const std::wstring& query);
+
+ protected:
+ QueryParser query_parser_;
+};
+
+}; // namespace
+
+// Test helper: Convert a user query string to a SQLite query string.
+std::wstring QueryParserTest::QueryToString(const std::wstring& query) {
+ std::wstring sqlite_query;
+ query_parser_.ParseQuery(query, &sqlite_query);
+ return sqlite_query;
+}
+
+// Basic multi-word queries, including prefix matching.
+TEST_F(QueryParserTest, SimpleQueries) {
+ EXPECT_EQ(L"", QueryToString(L" "));
+ EXPECT_EQ(L"singleword*", QueryToString(L"singleword"));
+ EXPECT_EQ(L"spacedout*", QueryToString(L" spacedout "));
+ EXPECT_EQ(L"foo* bar*", QueryToString(L"foo bar"));
+ // Short words aren't prefix matches. For Korean Hangul
+ // the minimum is 2 while for other scripts, it's 3.
+ EXPECT_EQ(L"f b", QueryToString(L" f b"));
+ // KA JANG
+ EXPECT_EQ(L"\xAC00 \xC7A5", QueryToString(L" \xAC00 \xC7A5"));
+ EXPECT_EQ(L"foo* bar*", QueryToString(L" foo bar "));
+ // KA-JANG BICH-GO
+ EXPECT_EQ(L"\xAC00\xC7A5* \xBE5B\xACE0*",
+ QueryToString(L"\xAC00\xC7A5 \xBE5B\xACE0"));
+}
+
+// Quoted substring parsing.
+TEST_F(QueryParserTest, Quoted) {
+ EXPECT_EQ(L"\"Quoted\"", QueryToString(L"\"Quoted\"")); // ASCII quotes
+ EXPECT_EQ(L"\"miss end\"", QueryToString(L"\"miss end")); // Missing end quotes
+ EXPECT_EQ(L"miss* beg*", QueryToString(L"miss beg\"")); // Missing begin quotes
+ EXPECT_EQ(L"\"Many\" \"quotes\"", QueryToString(L"\"Many \"\"quotes")); // Weird formatting
+}
+
+// Apostrophes within words should be preserved, but otherwise stripped.
+TEST_F(QueryParserTest, Apostrophes) {
+ EXPECT_EQ(L"foo* bar's*", QueryToString(L"foo bar's"));
+ EXPECT_EQ(L"l'foo*", QueryToString(L"l'foo"));
+ EXPECT_EQ(L"foo*", QueryToString(L"'foo"));
+}
+
+// Special characters.
+TEST_F(QueryParserTest, SpecialChars) {
+ EXPECT_EQ(L"foo* the* bar*", QueryToString(L"!#:/*foo#$*;'* the!#:/*bar"));
+}
+
+TEST_F(QueryParserTest, NumWords) {
+ TestData data[] = {
+ { L"blah", 1 },
+ { L"foo \"bar baz\"", 3 },
+ { L"foo \"baz\"", 2 },
+ { L"foo \"bar baz\" blah", 4 },
+ };
+
+ for (int i = 0; i < arraysize(data); ++i) {
+ std::wstring query_string;
+ EXPECT_EQ(data[i].expected_word_count,
+ query_parser_.ParseQuery(data[i].input, &query_string));
+ }
+}
+
+TEST_F(QueryParserTest, ParseQueryNodesAndMatch) {
+ struct TestData2 {
+ const std::wstring query;
+ const std::wstring text;
+ const bool matches;
+ } data[] = {
+ { L"blah", L"blah", true },
+ { L"blah", L"foo", false },
+ { L"blah", L"blahblah", true },
+ { L"blah", L"foo blah", true },
+ { L"foo blah", L"blah", false },
+ { L"foo blah", L"blahx foobar", true },
+ { L"\"foo blah\"", L"foo blah", true },
+ { L"\"foo blah\"", L"foox blahx", false },
+ { L"\"foo blah\"", L"foo blah", true },
+ { L"\"foo blah\"", L"\"foo blah\"", true },
+ { L"foo blah", L"\"foo bar blah\"", true },
+ };
+ for (int i = 0; i < arraysize(data); ++i) {
+ std::vector<std::wstring> results;
+ QueryParser parser;
+ ScopedVector<QueryNode> query_nodes;
+ parser.ParseQuery(data[i].query, &query_nodes.get());
+ ASSERT_EQ(data[i].matches,
+ parser.DoesQueryMatch(data[i].text, query_nodes.get()));
+ }
+} \ No newline at end of file
diff --git a/chrome/browser/history/redirect_uitest.cc b/chrome/browser/history/redirect_uitest.cc
new file mode 100644
index 0000000..75fed14
--- /dev/null
+++ b/chrome/browser/history/redirect_uitest.cc
@@ -0,0 +1,254 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Navigates the browser to server and client redirect pages and makes sure
+// that the correct redirects are reflected in the history database. Errors
+// here might indicate that WebKit changed the calls our glue layer gets in
+// the case of redirects. It may also mean problems with the history system.
+
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+// TODO(creis): Remove win_util when finished debugging ClientServerServer.
+#include "base/win_util.h"
+#include "chrome/test/automation/tab_proxy.h"
+#include "chrome/test/ui/ui_test.h"
+#include "net/base/net_util.h"
+#include "net/url_request/url_request_unittest.h"
+
+namespace {
+
+const wchar_t kDocRoot[] = L"chrome/test/data";
+
+class RedirectTest : public UITest {
+ protected:
+ RedirectTest() : UITest() {
+ }
+};
+
+} // namespace
+
+// Tests a single server redirect
+TEST_F(RedirectTest, Server) {
+ TestServer server(kDocRoot);
+
+ GURL final_url = server.TestServerPageW(std::wstring());
+ GURL first_url = server.TestServerPageW(
+ std::wstring(L"server-redirect?") + UTF8ToWide(final_url.spec()));
+
+ NavigateToURL(first_url);
+
+ scoped_ptr<TabProxy> tab_proxy(GetActiveTab());
+ ASSERT_TRUE(tab_proxy.get());
+
+ std::vector<GURL> redirects;
+ ASSERT_TRUE(tab_proxy->GetRedirectsFrom(first_url, &redirects));
+
+ ASSERT_EQ(1, redirects.size());
+ EXPECT_EQ(final_url.spec(), redirects[0].spec());
+}
+
+// Tests a single client redirect.
+TEST_F(RedirectTest, Client) {
+ TestServer server(kDocRoot);
+
+ GURL final_url = server.TestServerPageW(std::wstring());
+ GURL first_url = server.TestServerPageW(
+ std::wstring(L"client-redirect?") + UTF8ToWide(final_url.spec()));
+
+ // We need the sleep for the client redirects, because it appears as two
+ // page visits in the browser.
+ NavigateToURL(first_url);
+ Sleep(kWaitForActionMsec);
+
+ scoped_ptr<TabProxy> tab_proxy(GetActiveTab());
+ ASSERT_TRUE(tab_proxy.get());
+
+ std::vector<GURL> redirects;
+ ASSERT_TRUE(tab_proxy->GetRedirectsFrom(first_url, &redirects));
+
+ ASSERT_EQ(1, redirects.size());
+ EXPECT_EQ(final_url.spec(), redirects[0].spec());
+}
+
+TEST_F(RedirectTest, ClientEmptyReferer) {
+ TestServer server(kDocRoot);
+
+ GURL final_url = server.TestServerPageW(std::wstring());
+ std::wstring test_file = test_data_directory_;
+ file_util::AppendToPath(&test_file, L"file_client_redirect.html");
+ GURL first_url = net_util::FilePathToFileURL(test_file);
+
+ NavigateToURL(first_url);
+ std::vector<GURL> redirects;
+ // We need the sleeps for the client redirects, because it appears as two
+ // page visits in the browser. And note for this test the browser actually
+ // loads the html file on disk, rather than just getting a response from
+ // the TestServer.
+ for (int i = 0; i < 10; ++i) {
+ Sleep(kWaitForActionMaxMsec / 10);
+ scoped_ptr<TabProxy> tab_proxy(GetActiveTab());
+ ASSERT_TRUE(tab_proxy.get());
+ ASSERT_TRUE(tab_proxy->GetRedirectsFrom(first_url, &redirects));
+ if (!redirects.empty())
+ break;
+ }
+
+ EXPECT_EQ(1, redirects.size());
+ EXPECT_EQ(final_url.spec(), redirects[0].spec());
+}
+
+// Tests to make sure a location change when a pending redirect exists isn't
+// flagged as a redirect.
+TEST_F(RedirectTest, ClientCancelled) {
+ std::wstring first_path = test_data_directory_;
+ file_util::AppendToPath(&first_path, L"cancelled_redirect_test.html");
+ GURL first_url = net_util::FilePathToFileURL(first_path);
+
+ NavigateToURL(first_url);
+ Sleep(kWaitForActionMsec);
+
+ scoped_ptr<TabProxy> tab_proxy(GetActiveTab());
+ ASSERT_TRUE(tab_proxy.get());
+
+ std::vector<GURL> redirects;
+ ASSERT_TRUE(tab_proxy->GetRedirectsFrom(first_url, &redirects));
+
+ // There should be no redirects from first_url, because the anchor location
+ // change that occurs should not be flagged as a redirect and the meta-refresh
+ // won't have fired yet.
+ ASSERT_EQ(0, redirects.size());
+ GURL current_url;
+ ASSERT_TRUE(tab_proxy->GetCurrentURL(&current_url));
+
+ // Need to test final path and ref separately since constructing a file url
+ // containing an anchor using FilePathToFileURL will escape the anchor as
+ // %23, but in current_url the anchor will be '#'.
+ std::string final_ref = "myanchor";
+ std::wstring current_path;
+ ASSERT_TRUE(net_util::FileURLToFilePath(current_url, &current_path));
+ // Path should remain unchanged.
+ EXPECT_EQ(StringToLowerASCII(first_path), StringToLowerASCII(current_path));
+ EXPECT_EQ(final_ref, current_url.ref());
+}
+
+// Tests a client->server->server redirect
+// TODO(creis): This is disabled temporarily while I figure out why it is
+// failing.
+TEST_F(RedirectTest, DISABLED_ClientServerServer) {
+ TestServer server(kDocRoot);
+
+ GURL final_url = server.TestServerPageW(std::wstring());
+ GURL next_to_last = server.TestServerPageW(
+ std::wstring(L"server-redirect?") + UTF8ToWide(final_url.spec()));
+ GURL second_url = server.TestServerPageW(
+ std::wstring(L"server-redirect?") + UTF8ToWide(next_to_last.spec()));
+ GURL first_url = server.TestServerPageW(
+ std::wstring(L"client-redirect?") + UTF8ToWide(second_url.spec()));
+ std::vector<GURL> redirects;
+
+ // We need the sleep for the client redirects, because it appears as two
+ // page visits in the browser.
+ NavigateToURL(first_url);
+
+ for (int i = 0; i < 10; ++i) {
+ Sleep(kWaitForActionMaxMsec / 10);
+ scoped_ptr<TabProxy> tab_proxy(GetActiveTab());
+ ASSERT_TRUE(tab_proxy.get());
+ ASSERT_TRUE(tab_proxy->GetRedirectsFrom(first_url, &redirects));
+ if (!redirects.empty())
+ break;
+ }
+
+ ASSERT_EQ(3, redirects.size());
+ EXPECT_EQ(second_url.spec(), redirects[0].spec());
+ EXPECT_EQ(next_to_last.spec(), redirects[1].spec());
+ EXPECT_EQ(final_url.spec(), redirects[2].spec());
+}
+
+// Tests that the "#reference" gets preserved across server redirects.
+TEST_F(RedirectTest, ServerReference) {
+ TestServer server(kDocRoot);
+
+ const std::string ref("reference");
+
+ GURL final_url = server.TestServerPageW(std::wstring());
+ GURL initial_url = server.TestServerPageW(
+ std::wstring(L"server-redirect?") + UTF8ToWide(final_url.spec()) +
+ L"#" + UTF8ToWide(ref));
+
+ NavigateToURL(initial_url);
+
+ GURL url = GetActiveTabURL();
+ EXPECT_EQ(ref, url.ref());
+}
+
+// Test that redirect from http:// to file:// :
+// A) does not crash the browser or confuse the redirect chain, see bug 1080873
+// B) does not take place.
+TEST_F(RedirectTest, NoHttpToFile) {
+ TestServer server(kDocRoot);
+ std::wstring test_file = test_data_directory_;
+ file_util::AppendToPath(&test_file, L"http_to_file.html");
+ GURL file_url = net_util::FilePathToFileURL(test_file);
+
+ GURL initial_url = server.TestServerPageW(
+ std::wstring(L"client-redirect?") + UTF8ToWide(file_url.spec()));
+
+ NavigateToURL(initial_url);
+ // UITest will check for crashes. We make sure the title doesn't match the
+ // title from the file, because the nav should not have taken place.
+ scoped_ptr<TabProxy> tab_proxy(GetActiveTab());
+ ASSERT_TRUE(tab_proxy.get());
+ std::wstring actual_title;
+ tab_proxy->GetTabTitle(&actual_title);
+ EXPECT_NE(L"File!", actual_title);
+}
+
+// Ensures that non-user initiated location changes (within page) are
+// flagged as client redirects. See bug 1139823.
+TEST_F(RedirectTest, ClientFragments) {
+ TestServer server(kDocRoot);
+ std::wstring test_file = test_data_directory_;
+ file_util::AppendToPath(&test_file, L"ref_redirect.html");
+ GURL first_url = net_util::FilePathToFileURL(test_file);
+ std::vector<GURL> redirects;
+
+ NavigateToURL(first_url);
+ for (int i = 0; i < 10; ++i) {
+ Sleep(kWaitForActionMaxMsec / 10);
+ scoped_ptr<TabProxy> tab_proxy(GetActiveTab());
+ ASSERT_TRUE(tab_proxy.get());
+ ASSERT_TRUE(tab_proxy->GetRedirectsFrom(first_url, &redirects));
+ if (!redirects.empty())
+ break;
+ }
+
+ EXPECT_EQ(1, redirects.size());
+ EXPECT_EQ(first_url.spec() + "#myanchor", redirects[0].spec());
+} \ No newline at end of file
diff --git a/chrome/browser/history/snippet.cc b/chrome/browser/history/snippet.cc
new file mode 100644
index 0000000..ab7cb40
--- /dev/null
+++ b/chrome/browser/history/snippet.cc
@@ -0,0 +1,297 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/snippet.h"
+
+#include <algorithm>
+
+#include "base/logging.h"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "unicode/brkiter.h"
+#include "unicode/utext.h"
+#include "unicode/utf8.h"
+
+namespace {
+
+bool PairFirstLessThan(const std::pair<int,int>& a,
+ const std::pair<int,int>& b) {
+ return a.first < b.first;
+}
+
+// Combines all pairs after offset in match_positions that are contained
+// or touch the pair at offset.
+void CoalescePositionsFrom(size_t offset,
+ Snippet::MatchPositions* match_positions) {
+ DCHECK(offset < match_positions->size());
+ std::pair<int,int>& pair((*match_positions)[offset]);
+ ++offset;
+ while (offset < match_positions->size() &&
+ pair.second >= (*match_positions)[offset].first) {
+ pair.second = std::max(pair.second, (*match_positions)[offset].second);
+ match_positions->erase(match_positions->begin() + offset);
+ }
+}
+
+// Makes sure there is a pair in match_positions that contains the specified
+// range. This keeps the pairs ordered in match_positions by first, and makes
+// sure none of the pairs in match_positions touch each other.
+void AddMatch(int start, int end, Snippet::MatchPositions* match_positions) {
+ DCHECK(start < end && match_positions);
+ std::pair<int,int> pair(start, end);
+ if (match_positions->empty()) {
+ match_positions->push_back(pair);
+ return;
+ }
+ // There's at least one match. Find the position of the new match,
+ // potentially extending pairs around it.
+ Snippet::MatchPositions::iterator i =
+ std::lower_bound(match_positions->begin(), match_positions->end(),
+ pair, &PairFirstLessThan);
+ if (i != match_positions->end() && i->first == start) {
+ // Match not at the end and there is already a pair with the same
+ // start.
+ if (end > i->second) {
+ // New pair extends beyond existing pair. Extend existing pair and
+ // coalesce matches after it.
+ i->second = end;
+ CoalescePositionsFrom(i - match_positions->begin(), match_positions);
+ } // else case, new pair completely contained in existing pair, nothing
+ // to do.
+ } else if (i == match_positions->begin()) {
+ // Match at the beginning and the first pair doesn't have the same
+ // start. Insert new pair and coalesce matches after it.
+ match_positions->insert(i, pair);
+ CoalescePositionsFrom(0, match_positions);
+ } else {
+ // Not at the beginning (but may be at the end).
+ --i;
+ if (start <= i->second && end > i->second) {
+ // Previous element contains match. Extend it and coalesce.
+ i->second = end;
+ CoalescePositionsFrom(i - match_positions->begin(), match_positions);
+ } else if (end > i->second) {
+ // Region doesn't touch previous element. See if region touches current
+ // element.
+ ++i;
+ if (i == match_positions->end() || end < i->first) {
+ match_positions->insert(i, pair);
+ } else {
+ i->first = start;
+ i->second = end;
+ CoalescePositionsFrom(i - match_positions->begin(), match_positions);
+ }
+ }
+ }
+}
+
+// Converts an index in a utf8 string into the index in the corresponding wide
+// string and returns the wide index. This is intended to be called in a loop
+// iterating through a utf8 string.
+//
+// utf8_string: the utf8 string.
+// utf8_length: length of the utf8 string.
+// offset: the utf8 offset to convert.
+// utf8_pos: current offset in the utf8 string. This is modified and on return
+// matches offset.
+// wide_pos: current index in the wide string. This is the same as the return
+// value.
+int AdvanceAndReturnWidePos(const char* utf8_string,
+ int utf8_length,
+ int offset,
+ int* utf8_pos,
+ int* wide_pos) {
+ DCHECK(offset >= *utf8_pos && offset <= utf8_length);
+
+ UChar32 wide_char;
+ while (*utf8_pos < offset) {
+ U8_NEXT(utf8_string, *utf8_pos, utf8_length, wide_char);
+ *wide_pos += (wide_char <= 0xFFFF) ? 1 : 2;
+ }
+ return *wide_pos;
+}
+
+// Given a character break iterator over a UTF-8 string, set the iterator
+// position to |*utf8_pos| and move by |count| characters. |count| can
+// be either positive or negative.
+void MoveByNGraphemes(BreakIterator* bi, int count, int* utf8_pos) {
+ // Ignore the return value. A side effect of the current position
+ // being set at or following |*utf8_pos| is exploited here.
+ // It's simpler than calling following(n) and then previous().
+ // isBoundary() is not very fast, but should be good enough for the
+ // snippet generation. If not, revisit the way we scan in ComputeSnippet.
+ bi->isBoundary(*utf8_pos);
+ bi->next(count);
+ *utf8_pos = static_cast<int>(bi->current());
+}
+
+// The amount of context to include for a given hit. Note that it's counted
+// in terms of graphemes rather than bytes.
+const int kSnippetContext = 50;
+
+// Returns true if next match falls within a snippet window
+// from the previous match. The window size is counted in terms
+// of graphemes rather than bytes in UTF-8.
+bool IsNextMatchWithinSnippetWindow(BreakIterator* bi,
+ int previous_match_end,
+ int next_match_start) {
+ // If it's within a window in terms of bytes, it's certain
+ // that it's within a window in terms of graphemes as well.
+ if (next_match_start < previous_match_end + kSnippetContext)
+ return true;
+ bi->isBoundary(previous_match_end);
+ // An alternative to this is to call |bi->next()| at most
+ // kSnippetContext times, compare |bi->current()| with |next_match_start|
+ // after each call and return early if possible. There are other
+ // heuristics to speed things up if necessary, but it's not likely that
+ // we need to bother.
+ bi->next(kSnippetContext);
+ int64_t current = bi->current();
+ return (next_match_start < current || current == BreakIterator::DONE);
+}
+
+} // namespace
+
+// static
+void Snippet::ExtractMatchPositions(const std::string& offsets_str,
+ const std::string& column_num,
+ MatchPositions* match_positions) {
+ DCHECK(match_positions);
+ if (offsets_str.empty())
+ return;
+ std::vector<std::string> offsets;
+ SplitString(offsets_str, ' ', &offsets);
+ // SQLite offsets are sets of four integers:
+ // column, query term, match offset, match length
+ // Matches within a string are marked by (start, end) pairs.
+ for (size_t i = 0; i < offsets.size() - 3; i += 4) {
+ if (offsets[i] != column_num)
+ continue;
+ const int start = atoi(offsets[i+2].c_str());
+ const int end = start + atoi(offsets[i+3].c_str());
+ AddMatch(start, end, match_positions);
+ }
+}
+
+// static
+void Snippet::ConvertMatchPositionsToWide(
+ const std::string& utf8_string,
+ Snippet::MatchPositions* match_positions) {
+ DCHECK(match_positions);
+ int utf8_pos = 0;
+ int wide_pos = 0;
+ const char* utf8_cstring = utf8_string.c_str();
+ const int utf8_length = static_cast<int>(utf8_string.size());
+ for (Snippet::MatchPositions::iterator i = match_positions->begin();
+ i != match_positions->end(); ++i) {
+ i->first = AdvanceAndReturnWidePos(utf8_cstring, utf8_length,
+ i->first, &utf8_pos, &wide_pos);
+ i->second =
+ AdvanceAndReturnWidePos(utf8_cstring, utf8_length, i->second, &utf8_pos,
+ &wide_pos);
+ }
+}
+
+void Snippet::ComputeSnippet(const MatchPositions& match_positions,
+ const std::string& document) {
+ // The length of snippets we try to produce.
+ // We can generate longer snippets but stop once we cross kSnippetMaxLength.
+ const size_t kSnippetMaxLength = 200;
+
+
+ const std::wstring kEllipsis = L" ... ";
+
+ // Grab the size as an int to cut down on casts later.
+ const int document_size = static_cast<int>(document.size());
+
+ UText* document_utext = NULL;
+ UErrorCode status = U_ZERO_ERROR;
+ document_utext = utext_openUTF8(document_utext, document.data(),
+ document_size, &status);
+ // Locale does not matter because there's no per-locale customization
+ // for character iterator.
+ scoped_ptr<BreakIterator> bi(
+ BreakIterator::createCharacterInstance(Locale::getDefault(), status));
+ bi->setText(document_utext, status);
+ DCHECK(U_SUCCESS(status));
+
+ // We build the snippet by iterating through the matches and then grabbing
+ // context around each match. If matches are near enough each other (within
+ // kSnippetContext), we skip the "..." between them.
+ std::wstring snippet;
+ int start = 0;
+ for (size_t i = 0; i < match_positions.size(); ++i) {
+ // Some shorter names for the current match.
+ const int match_start = match_positions[i].first;
+ const int match_end = match_positions[i].second;
+
+ // Add the context, if any, to show before the match.
+ int context_start = match_start;
+ MoveByNGraphemes(bi.get(), -kSnippetContext, &context_start);
+ start = std::max(start, context_start);
+ if (start < match_start) {
+ if (start > 0)
+ snippet += kEllipsis;
+ snippet += UTF8ToWide(document.substr(start, match_start - start));
+ }
+
+ // Add the match.
+ matches_.push_back(std::make_pair(static_cast<int>(snippet.size()), 0));
+ snippet += UTF8ToWide(document.substr(match_start,
+ match_end - match_start));
+ matches_.back().second = static_cast<int>(snippet.size());
+
+ // Compute the context, if any, to show after the match.
+ int end;
+ // Check if the next match falls within our snippet window.
+ if (i + 1 < match_positions.size() &&
+ IsNextMatchWithinSnippetWindow(bi.get(), match_end,
+ match_positions[i + 1].first)) {
+ // Yes, it's within the window. Make the end context extend just up
+ // to the next match.
+ end = match_positions[i + 1].first;
+ snippet += UTF8ToWide(document.substr(match_end, end - match_end));
+ } else {
+ // No, there's either no next match or the next match is too far away.
+ end = match_end;
+ MoveByNGraphemes(bi.get(), kSnippetContext, &end);
+ snippet += UTF8ToWide(document.substr(match_end, end - match_end));
+ if (end < document_size)
+ snippet += kEllipsis;
+ }
+ start = end;
+
+ // Stop here if we have enough snippet computed.
+ if (snippet.size() >= kSnippetMaxLength)
+ break;
+ }
+
+ utext_close(document_utext);
+ swap(text_, snippet);
+}
diff --git a/chrome/browser/history/snippet.h b/chrome/browser/history/snippet.h
new file mode 100644
index 0000000..db002fa
--- /dev/null
+++ b/chrome/browser/history/snippet.h
@@ -0,0 +1,91 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// This module computes snippets of queries based on hits in the documents
+// for display in history search results.
+
+#ifndef CHROME_BROWSER_HISTORY_SNIPPET_H__
+#define CHROME_BROWSER_HISTORY_SNIPPET_H__
+
+#include <vector>
+
+class Snippet {
+ public:
+ // Each pair in MatchPositions is the [begin, end) positions of a match
+ // within a string.
+ typedef std::vector<std::pair<int, int> > MatchPositions;
+
+ // Parses an offsets string as returned from a sqlite full text index. An
+ // offsets string encodes information about why a row matched a text query.
+ // The information is encoded in the string as a set of matches, where each
+ // match consists of the column, term-number, location, and length of the
+ // match. Each element of the match is separated by a space, as is each match
+ // from other matches.
+ //
+ // This method adds the start and end of each match whose column is
+ // column_num to match_positions. The pairs are ordered based on first,
+ // with no overlapping elements.
+ //
+ // NOTE: the positions returned are in terms of UTF8 encoding. To convert the
+ // offsets to wide, use ConvertMatchPositionsToWide.
+ static void ExtractMatchPositions(const std::string& offsets_str,
+ const std::string& column_num,
+ MatchPositions* match_positions);
+
+ // Converts match positions as returned from ExtractMatchPositions to be in
+ // terms of a wide string.
+ static void ConvertMatchPositionsToWide(
+ const std::string& utf8_string,
+ Snippet::MatchPositions* match_positions);
+
+ // Given |matches|, the match positions within |document|, compute the snippet
+ // for the document.
+ // Note that |document| is UTF-8 and the offsets in |matches| are byte
+ // offsets.
+ void ComputeSnippet(const MatchPositions& matches,
+ const std::string& document);
+
+ const std::wstring& text() const { return text_; }
+ const MatchPositions& matches() const { return matches_; }
+
+ // Efficiently swaps the contents of this snippet with the other.
+ void Swap(Snippet* other) {
+ text_.swap(other->text_);
+ matches_.swap(other->matches_);
+ }
+
+ private:
+ // The text of the snippet.
+ std::wstring text_;
+
+ // The matches within text_.
+ MatchPositions matches_;
+};
+
+#endif // CHROME_BROWSER_HISTORY_SNIPPET_H__
diff --git a/chrome/browser/history/snippet_unittest.cc b/chrome/browser/history/snippet_unittest.cc
new file mode 100644
index 0000000..9828e24
--- /dev/null
+++ b/chrome/browser/history/snippet_unittest.cc
@@ -0,0 +1,279 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/snippet.h"
+
+#include <algorithm>
+
+#include "base/logging.h"
+#include "base/string_util.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+// A sample document to compute snippets of.
+// The \x bits after the first "Google" are UTF-8 of U+2122 TRADE MARK SIGN,
+// and are useful for verifying we don't screw up in UTF-8/UTF-16 conversion.
+const char* kSampleDocument = "Google\xe2\x84\xa2 Terms of Service "
+"Welcome to Google! "
+"1. Your relationship with Google "
+"1.1 Your use of Google's products, software, services and web sites "
+"(referred to collectively as the \"Services\" in this document and excluding "
+"any services provided to you by Google under a separate written agreement) "
+"is subject to the terms of a legal agreement between you and Google. "
+"\"Google\" means Google Inc., whose principal place of business is at 1600 "
+"Amphitheatre Parkway, Mountain View, CA 94043, United States. This document "
+"explains how the agreement is made up, and sets out some of the terms of "
+"that agreement.";
+
+};
+
+// Thai sample taken from http://www.google.co.th/intl/th/privacy.html
+// TODO(jungshik) : Add more samples (e.g. Hindi) after porting
+// ICU 4.0's character iterator changes to our copy of ICU 3.8 to get
+// grapheme clusters in Indic scripts handled more reasonably.
+const char* kThaiSample = "Google \xE0\xB9\x80\xE0\xB8\x81\xE0\xB9\x87"
+"\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xA7"
+"\xE0\xB8\xA1 \xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9"
+"\xE0\xB8\xA5\xE0\xB8\xAA\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x99\xE0\xB8\x9A"
+"\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5 \xE0\xB9\x80\xE0\xB8\xA1"
+"\xE0\xB8\xB7\xE0\xB9\x88\xE0\xB8\xAD\xE0\xB8\x84\xE0\xB8\xB8\xE0\xB8\x93"
+"\xE0\xB8\xA5\xE0\xB8\x87\xE0\xB8\x97\xE0\xB8\xB0\xE0\xB9\x80\xE0\xB8\x9A"
+"\xE0\xB8\xB5\xE0\xB8\xA2\xE0\xB8\x99\xE0\xB9\x80\xE0\xB8\x9E\xE0\xB8\xB7"
+"\xE0\xB9\x88\xE0\xB8\xAD\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x89\xE0\xB8\x9A"
+"\xE0\xB8\xA3\xE0\xB8\xB4\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3\xE0\xB8\x82"
+"\xE0\xB8\xAD\xE0\xB8\x87 Google \xE0\xB8\xAB\xE0\xB8\xA3\xE0\xB8\xB7"
+"\xE0\xB8\xAD\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89\xE0\xB8\x82\xE0\xB9\x89"
+"\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9\xE0\xB8\xA5\xE0\xB8\x94\xE0\xB8\xB1"
+"\xE0\xB8\x87\xE0\xB8\x81\xE0\xB8\xA5\xE0\xB9\x88\xE0\xB8\xB2\xE0\xB8\xA7"
+"\xE0\xB9\x82\xE0\xB8\x94\xE0\xB8\xA2\xE0\xB8\xAA\xE0\xB8\xA1\xE0\xB8\xB1"
+"\xE0\xB8\x84\xE0\xB8\xA3\xE0\xB9\x83\xE0\xB8\x88 \xE0\xB9\x80\xE0\xB8\xA3"
+"\xE0\xB8\xB2\xE0\xB8\xAD\xE0\xB8\xB2\xE0\xB8\x88\xE0\xB8\xA3\xE0\xB8\xA7"
+"\xE0\xB8\xA1\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9"
+"\xE0\xB8\xA5\xE0\xB8\xAA\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x99\xE0\xB8\x9A"
+"\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5\xE0\xB8\x97\xE0\xB8\xB5"
+"\xE0\xB9\x88\xE0\xB9\x80\xE0\xB8\x81\xE0\xB9\x87\xE0\xB8\x9A\xE0\xB8\xA3"
+"\xE0\xB8\xA7\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\xA1\xE0\xB8\x88"
+"\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\x84\xE0\xB8\xB8\xE0\xB8\x93\xE0\xB9\x80"
+"\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\xB1\xE0\xB8\x9A"
+"\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9\xE0\xB8\xA5"
+"\xE0\xB8\x88\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xB4"
+"\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3\xE0\xB8\xAD\xE0\xB8\xB7\xE0\xB9\x88"
+"\xE0\xB8\x99\xE0\xB8\x82\xE0\xB8\xAD\xE0\xB8\x87 Google \xE0\xB8\xAB"
+"\xE0\xB8\xA3\xE0\xB8\xB7\xE0\xB8\xAD\xE0\xB8\x9A\xE0\xB8\xB8\xE0\xB8\x84"
+"\xE0\xB8\x84\xE0\xB8\xA5\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB8\xAA"
+"\xE0\xB8\xB2\xE0\xB8\xA1 \xE0\xB9\x80\xE0\xB8\x9E\xE0\xB8\xB7\xE0\xB9\x88"
+"\xE0\xB8\xAD\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89\xE0\xB8\x9C\xE0\xB8\xB9"
+"\xE0\xB9\x89\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x89\xE0\xB9\x84\xE0\xB8\x94"
+"\xE0\xB9\x89\xE0\xB8\xA3\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB8\x9B\xE0\xB8\xA3"
+"\xE0\xB8\xB0\xE0\xB8\xAA\xE0\xB8\x9A\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3"
+"\xE0\xB8\x93\xE0\xB9\x8C\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB8\x94"
+"\xE0\xB8\xB5\xE0\xB8\x82\xE0\xB8\xB6\xE0\xB9\x89\xE0\xB8\x99 \xE0\xB8\xA3"
+"\xE0\xB8\xA7\xE0\xB8\xA1\xE0\xB8\x97\xE0\xB8\xB1\xE0\xB9\x89\xE0\xB8\x87"
+"\xE0\xB8\x9B\xE0\xB8\xA3\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB9\x81\xE0\xB8\x95"
+"\xE0\xB9\x88\xE0\xB8\x87\xE0\xB9\x80\xE0\xB8\x99\xE0\xB8\xB7\xE0\xB9\x89"
+"\xE0\xB8\xAD\xE0\xB8\xAB\xE0\xB8\xB2\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89"
+"\xE0\xB9\x80\xE0\xB8\xAB\xE0\xB8\xA1\xE0\xB8\xB2\xE0\xB8\xB0\xE0\xB8\xAA"
+"\xE0\xB8\xB3\xE0\xB8\xAB\xE0\xB8\xA3\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB8\x84"
+"\xE0\xB8\xB8\xE0\xB8\x93";
+
+// Comparator for sorting by the first element in a pair.
+bool ComparePair1st(const std::pair<int, int>& a,
+ const std::pair<int, int>& b) {
+ return a.first < b.first;
+}
+
+// For testing, we'll compute the match positions manually instead of using
+// sqlite's FTS matching. BuildSnippet returns the snippet for matching
+// |query| against |document|. Matches are surrounded by "**".
+std::wstring BuildSnippet(const std::string& document,
+ const std::string& query) {
+ // This function assumes that |document| does not contain
+ // any character for which lowercasing changes its length. Further,
+ // it's assumed that lowercasing only the ASCII-portion works for
+ // |document|. We need to add more test cases and change this function
+ // to be more generic depending on how we deal with 'folding for match'
+ // in history.
+ const std::string document_folded = StringToLowerASCII(std::string(document));
+
+ std::vector<std::string> query_words;
+ SplitString(query, ' ', &query_words);
+
+ // Manually construct match_positions of the document.
+ Snippet::MatchPositions match_positions;
+ match_positions.clear();
+ for (std::vector<std::string>::iterator qw = query_words.begin();
+ qw != query_words.end(); ++qw) {
+ // Insert all instances of this word into match_pairs.
+ std::string::size_type ofs = 0;
+ while ((ofs = document_folded.find(*qw, ofs)) != std::string::npos) {
+ match_positions.push_back(
+ std::make_pair(static_cast<int>(ofs),
+ static_cast<int>(ofs + qw->size())));
+ ofs += qw->size();
+ }
+ }
+ // Sort match_positions in order of increasing offset.
+ std::sort(match_positions.begin(), match_positions.end(), ComparePair1st);
+
+ // Compute the snippet.
+ Snippet snippet;
+ snippet.ComputeSnippet(match_positions, document);
+
+ // Now "highlight" all matches in the snippet with **.
+ std::wstring star_snippet;
+ Snippet::MatchPositions::const_iterator match;
+ size_t pos = 0;
+ for (match = snippet.matches().begin();
+ match != snippet.matches().end(); ++match) {
+ star_snippet += snippet.text().substr(pos, match->first - pos);
+ star_snippet += L"**";
+ star_snippet += snippet.text().substr(match->first,
+ match->second - match->first);
+ star_snippet += L"**";
+ pos = match->second;
+ }
+ star_snippet += snippet.text().substr(pos);
+
+ return star_snippet;
+}
+
+TEST(Snippets, SimpleQuery) {
+ ASSERT_EQ(L" ... eferred to collectively as the \"Services\" in this "
+ L"**document** and excluding any services provided to you by "
+ L"Goo ... ... way, Mountain View, CA 94043, United States. This "
+ L"**document** explains how the agreement is made up, and sets "
+ L"o ... ",
+ BuildSnippet(kSampleDocument, "document"));
+}
+
+// Test that two words that are near each other don't produce two elided bits.
+TEST(Snippets, NearbyWords) {
+ ASSERT_EQ(L" ... lace of business is at 1600 Amphitheatre Parkway, "
+ L"**Mountain** **View**, CA 94043, United States. This "
+ L"document explains ... ",
+ BuildSnippet(kSampleDocument, "mountain view"));
+}
+
+// The above tests already test that we get byte offsets correct, but here's
+// one that gets the "TM" in its snippet.
+TEST(Snippets, UTF8) {
+ ASSERT_EQ(" ... ogle\xe2\x84\xa2 Terms of Service Welcome to Google! "
+ "1. Your **relationship** with Google 1.1 Your use of Google's "
+ "products, so ... ",
+ WideToUTF8(BuildSnippet(kSampleDocument, "relationship")));
+}
+
+// Bug: 1274923
+TEST(Snippets, DISABLED_ThaiUTF8) {
+ // There are 3 instances of '\u0E43\u0E2B\u0E49'
+ // (\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89) in kThaiSample.
+ // The 1st is more than |kSniipetContext| graphemes away from the
+ // 2nd while the 2nd and 3rd are within that window. However, with
+ // the 2nd match added, the snippet goes over the size limit so that
+ // the snippet ends right before the 3rd match.
+ ASSERT_EQ(" ... "
+ " \xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9"
+ "\xE0\xB8\xA5\xE0\xB8\xAA\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x99"
+ "\xE0\xB8\x9A\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5 "
+ "\xE0\xB9\x80\xE0\xB8\xA1\xE0\xB8\xB7\xE0\xB9\x88\xE0\xB8\xAD"
+ "\xE0\xB8\x84\xE0\xB8\xB8\xE0\xB8\x93\xE0\xB8\xA5\xE0\xB8\x87"
+ "\xE0\xB8\x97\xE0\xB8\xB0\xE0\xB9\x80\xE0\xB8\x9A\xE0\xB8\xB5"
+ "\xE0\xB8\xA2\xE0\xB8\x99\xE0\xB9\x80\xE0\xB8\x9E\xE0\xB8\xB7"
+ "\xE0\xB9\x88\xE0\xB8\xAD\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x89"
+ "\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xB4\xE0\xB8\x81\xE0\xB8\xB2"
+ "\xE0\xB8\xA3\xE0\xB8\x82\xE0\xB8\xAD\xE0\xB8\x87 Google "
+ "\xE0\xB8\xAB\xE0\xB8\xA3\xE0\xB8\xB7\xE0\xB8\xAD**\xE0\xB9\x83"
+ "\xE0\xB8\xAB\xE0\xB9\x89**\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD"
+ "\xE0\xB8\xA1\xE0\xB8\xB9\xE0\xB8\xA5\xE0\xB8\x94\xE0\xB8\xB1"
+ "\xE0\xB8\x87\xE0\xB8\x81\xE0\xB8\xA5\xE0\xB9\x88\xE0\xB8\xB2"
+ "\xE0\xB8\xA7\xE0\xB9\x82\xE0\xB8\x94\xE0\xB8\xA2\xE0\xB8\xAA"
+ "\xE0\xB8\xA1\xE0\xB8\xB1\xE0\xB8\x84\xE0\xB8\xA3\xE0\xB9\x83"
+ "\xE0\xB8\x88 \xE0\xB9\x80\xE0\xB8\xA3\xE0\xB8\xB2\xE0\xB8\xAD"
+ "\xE0\xB8\xB2\xE0\xB8\x88\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\xA1"
+ "\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9"
+ "\xE0\xB8\xA5\xE0\xB8\xAA\xE0\xB9\x88\xE0\xB8\xA7\xE0\xB8\x99"
+ "\xE0\xB8\x9A\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5"
+ "\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB9\x80\xE0\xB8\x81"
+ "\xE0\xB9\x87\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\x9A"
+ "\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\xA1 ... ... "
+ "\xE0\xB8\x88\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\x84\xE0\xB8\xB8"
+ "\xE0\xB8\x93\xE0\xB9\x80\xE0\xB8\x82\xE0\xB9\x89\xE0\xB8\xB2"
+ "\xE0\xB8\x81\xE0\xB8\xB1\xE0\xB8\x9A\xE0\xB8\x82\xE0\xB9\x89"
+ "\xE0\xB8\xAD\xE0\xB8\xA1\xE0\xB8\xB9\xE0\xB8\xA5\xE0\xB8\x88"
+ "\xE0\xB8\xB2\xE0\xB8\x81\xE0\xB8\x9A\xE0\xB8\xA3\xE0\xB8\xB4"
+ "\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3\xE0\xB8\xAD\xE0\xB8\xB7"
+ "\xE0\xB9\x88\xE0\xB8\x99\xE0\xB8\x82\xE0\xB8\xAD\xE0\xB8\x87 "
+ "Google \xE0\xB8\xAB\xE0\xB8\xA3\xE0\xB8\xB7\xE0\xB8\xAD"
+ "\xE0\xB8\x9A\xE0\xB8\xB8\xE0\xB8\x84\xE0\xB8\x84\xE0\xB8\xA5"
+ "\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB8\xAA\xE0\xB8\xB2"
+ "\xE0\xB8\xA1 \xE0\xB9\x80\xE0\xB8\x9E\xE0\xB8\xB7\xE0\xB9\x88"
+ "\xE0\xB8\xAD**\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89**\xE0\xB8\x9C"
+ "\xE0\xB8\xB9\xE0\xB9\x89\xE0\xB9\x83\xE0\xB8\x8A\xE0\xB9\x89"
+ "\xE0\xB9\x84\xE0\xB8\x94\xE0\xB9\x89\xE0\xB8\xA3\xE0\xB8\xB1"
+ "\xE0\xB8\x9A\xE0\xB8\x9B\xE0\xB8\xA3\xE0\xB8\xB0\xE0\xB8\xAA"
+ "\xE0\xB8\x9A\xE0\xB8\x81\xE0\xB8\xB2\xE0\xB8\xA3\xE0\xB8\x93"
+ "\xE0\xB9\x8C\xE0\xB8\x97\xE0\xB8\xB5\xE0\xB9\x88\xE0\xB8\x94"
+ "\xE0\xB8\xB5\xE0\xB8\x82\xE0\xB8\xB6\xE0\xB9\x89\xE0\xB8\x99 "
+ "\xE0\xB8\xA3\xE0\xB8\xA7\xE0\xB8\xA1\xE0\xB8\x97\xE0\xB8\xB1"
+ "\xE0\xB9\x89\xE0\xB8\x87\xE0\xB8\x9B\xE0\xB8\xA3\xE0\xB8\xB1"
+ "\xE0\xB8\x9A\xE0\xB9\x81\xE0\xB8\x95\xE0\xB9\x88\xE0\xB8\x87"
+ "\xE0\xB9\x80\xE0\xB8\x99\xE0\xB8\xB7\xE0\xB9\x89\xE0\xB8\xAD"
+ "\xE0\xB8\xAB\xE0\xB8\xB2",
+ WideToUTF8(BuildSnippet(kThaiSample,
+ "\xE0\xB9\x83\xE0\xB8\xAB\xE0\xB9\x89")));
+}
+
+TEST(Snippets, ExtractMatchPositions) {
+ struct TestData {
+ const std::string offsets_string;
+ const int expected_match_count;
+ const int expected_matches[10];
+ } data[] = {
+ { "0 0 1 2 0 0 4 1 0 0 1 5", 1, { 1,6 } },
+ { "0 0 1 4 0 0 2 1", 1, { 1,5 } },
+ { "0 0 4 1 0 0 2 1", 2, { 2,3, 4,5 } },
+ { "0 0 0 1", 1, { 0,1 } },
+ { "0 0 0 1 0 0 0 2", 1, { 0,2 } },
+ { "0 0 1 1 0 0 1 2", 1, { 1,3 } },
+ { "0 0 1 2 0 0 4 3 0 0 3 1", 1, { 1,7 } },
+ { "0 0 1 4 0 0 2 5", 1, { 1,7 } },
+ { "0 0 1 2 0 0 1 1", 1, { 1,3 } },
+ { "0 0 1 1 0 0 5 2 0 0 10 1 0 0 3 10", 2, { 1,2, 3,13 } },
+ };
+ for (int i = 0; i < arraysize(data); ++i) {
+ Snippet::MatchPositions matches;
+ Snippet::ExtractMatchPositions(data[i].offsets_string, "0", &matches);
+ EXPECT_EQ(data[i].expected_match_count, matches.size());
+ for (int j = 0; j < data[i].expected_match_count; ++j) {
+ EXPECT_EQ(data[i].expected_matches[2 * j], matches[j].first);
+ EXPECT_EQ(data[i].expected_matches[2 * j + 1], matches[j].second);
+ }
+ }
+}
diff --git a/chrome/browser/history/starred_url_database.cc b/chrome/browser/history/starred_url_database.cc
new file mode 100644
index 0000000..7294222
--- /dev/null
+++ b/chrome/browser/history/starred_url_database.cc
@@ -0,0 +1,888 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/starred_url_database.h"
+
+#include "base/logging.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/query_parser.h"
+#include "chrome/browser/meta_table_helper.h"
+#include "chrome/common/scoped_vector.h"
+#include "chrome/common/sqlite_compiled_statement.h"
+#include "chrome/common/sqlite_utils.h"
+#include "chrome/common/stl_util-inl.h"
+
+// The following table is used to store star (aka bookmark) information. This
+// class derives from URLDatabase, which has its own schema.
+//
+// starred
+// id Unique identifier (primary key) for the entry.
+// type Type of entry, if 0 this corresponds to a URL, 1 for
+// a system grouping, 2 for a user created group, 3 for
+// other.
+// url_id ID of the url, only valid if type == 0
+// group_id ID of the group, only valid if type != 0. This id comes
+// from the UI and is NOT the same as id.
+// title User assigned title.
+// date_added Creation date.
+// visual_order Visual order within parent.
+// parent_id Group ID of the parent this entry is contained in, if 0
+// entry is not in a group.
+// date_modified Time the group was last modified. See comments in
+// StarredEntry::date_group_modified
+// NOTE: group_id and parent_id come from the UI, id is assigned by the
+// db.
+
+namespace history {
+
+namespace {
+
+// Fields used by FillInStarredEntry.
+#define STAR_FIELDS \
+ " starred.id, starred.type, starred.title, starred.date_added, " \
+ "starred.visual_order, starred.parent_id, urls.url, urls.id, " \
+ "starred.group_id, starred.date_modified "
+const char kHistoryStarFields[] = STAR_FIELDS;
+
+void FillInStarredEntry(SQLStatement* s, StarredEntry* entry) {
+ DCHECK(entry);
+ entry->id = s->column_int64(0);
+ switch (s->column_int(1)) {
+ case 0:
+ entry->type = history::StarredEntry::URL;
+ entry->url = GURL(s->column_string16(6));
+ break;
+ case 1:
+ entry->type = history::StarredEntry::BOOKMARK_BAR;
+ break;
+ case 2:
+ entry->type = history::StarredEntry::USER_GROUP;
+ break;
+ case 3:
+ entry->type = history::StarredEntry::OTHER;
+ break;
+ default:
+ NOTREACHED();
+ break;
+ }
+ entry->title = s->column_string16(2);
+ entry->date_added = Time::FromInternalValue(s->column_int64(3));
+ entry->visual_order = s->column_int(4);
+ entry->parent_group_id = s->column_int64(5);
+ entry->url_id = s->column_int64(7);
+ entry->group_id = s->column_int64(8);
+ entry->date_group_modified = Time::FromInternalValue(s->column_int64(9));
+}
+
+} // namespace
+
+StarredURLDatabase::StarredURLDatabase()
+ : is_starred_valid_(true), check_starred_integrity_on_mutation_(true) {
+}
+
+StarredURLDatabase::~StarredURLDatabase() {
+}
+
+bool StarredURLDatabase::InitStarTable() {
+ if (!DoesSqliteTableExist(GetDB(), "starred")) {
+ if (sqlite3_exec(GetDB(), "CREATE TABLE starred ("
+ "id INTEGER PRIMARY KEY,"
+ "type INTEGER NOT NULL DEFAULT 0,"
+ "url_id INTEGER NOT NULL DEFAULT 0,"
+ "group_id INTEGER NOT NULL DEFAULT 0,"
+ "title VARCHAR,"
+ "date_added INTEGER NOT NULL,"
+ "visual_order INTEGER DEFAULT 0,"
+ "parent_id INTEGER DEFAULT 0,"
+ "date_modified INTEGER DEFAULT 0 NOT NULL)",
+ NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED();
+ return false;
+ }
+ if (sqlite3_exec(GetDB(), "CREATE INDEX starred_index "
+ "ON starred(id,url_id)", NULL, NULL, NULL)) {
+ NOTREACHED();
+ return false;
+ }
+ // Add an entry that represents the bookmark bar. The title is ignored in
+ // the UI.
+ StarID bookmark_id = CreateStarredEntryRow(
+ 0, HistoryService::kBookmarkBarID, 0, L"bookmark-bar", Time::Now(), 0,
+ history::StarredEntry::BOOKMARK_BAR);
+ if (bookmark_id != HistoryService::kBookmarkBarID) {
+ NOTREACHED();
+ return false;
+ }
+
+ // Add an entry that represents other. The title is ignored in the UI.
+ StarID other_id = CreateStarredEntryRow(
+ 0, HistoryService::kBookmarkBarID + 1, 0, L"other", Time::Now(), 0,
+ history::StarredEntry::OTHER);
+ if (!other_id) {
+ NOTREACHED();
+ return false;
+ }
+ }
+
+ sqlite3_exec(GetDB(), "CREATE INDEX starred_group_index"
+ "ON starred(group_id)", NULL, NULL, NULL);
+ return true;
+}
+
+bool StarredURLDatabase::EnsureStarredIntegrity() {
+ if (!is_starred_valid_)
+ return false;
+
+ // Assume invalid, we'll set to true if succesful.
+ is_starred_valid_ = false;
+
+ std::set<StarredNode*> roots;
+ std::set<StarID> groups_with_duplicate_ids;
+ std::set<StarredNode*> unparented_urls;
+ std::set<StarID> empty_url_ids;
+
+ if (!BuildStarNodes(&roots, &groups_with_duplicate_ids, &unparented_urls,
+ &empty_url_ids)) {
+ return false;
+ }
+
+ // The table may temporarily get into an invalid state while updating the
+ // integrity.
+ bool org_check_starred_integrity_on_mutation =
+ check_starred_integrity_on_mutation_;
+ check_starred_integrity_on_mutation_ = false;
+ is_starred_valid_ =
+ EnsureStarredIntegrityImpl(&roots, groups_with_duplicate_ids,
+ &unparented_urls, empty_url_ids);
+ check_starred_integrity_on_mutation_ =
+ org_check_starred_integrity_on_mutation;
+
+ STLDeleteElements(&roots);
+ STLDeleteElements(&unparented_urls);
+ return is_starred_valid_;
+}
+
+StarID StarredURLDatabase::GetStarIDForEntry(const StarredEntry& entry) {
+ if (entry.type == StarredEntry::URL) {
+ URLRow url_row;
+ URLID url_id = GetRowForURL(entry.url, &url_row);
+ if (!url_id)
+ return 0;
+ return url_row.star_id();
+ }
+ return GetStarIDForGroupID(entry.group_id);
+}
+
+StarID StarredURLDatabase::GetStarIDForGroupID(UIStarID group_id) {
+ DCHECK(group_id);
+
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT id FROM starred WHERE group_id = ?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, group_id);
+ if (statement->step() == SQLITE_ROW)
+ return statement->column_int64(0);
+ return 0;
+}
+
+void StarredURLDatabase::DeleteStarredEntry(
+ StarID star_id,
+ std::set<GURL>* unstarred_urls,
+ std::vector<StarredEntry>* deleted_entries) {
+ DeleteStarredEntryImpl(star_id, unstarred_urls, deleted_entries);
+ if (check_starred_integrity_on_mutation_)
+ CheckStarredIntegrity();
+}
+
+bool StarredURLDatabase::UpdateStarredEntry(StarredEntry* entry) {
+ // Determine the ID used by the database.
+ const StarID id = GetStarIDForEntry(*entry);
+ if (!id) {
+ NOTREACHED() << "request to update unknown star entry";
+ return false;
+ }
+
+ StarredEntry original_entry;
+ if (!GetStarredEntry(id, &original_entry)) {
+ NOTREACHED() << "Unknown star entry";
+ return false;
+ }
+
+ if (entry->parent_group_id != original_entry.parent_group_id) {
+ // Parent has changed.
+ if (original_entry.parent_group_id) {
+ AdjustStarredVisualOrder(original_entry.parent_group_id,
+ original_entry.visual_order, -1);
+ }
+ if (entry->parent_group_id) {
+ AdjustStarredVisualOrder(entry->parent_group_id, entry->visual_order, 1);
+ }
+ } else if (entry->visual_order != original_entry.visual_order &&
+ entry->parent_group_id) {
+ // Same parent, but visual order changed.
+ // Shift everything down from the old location.
+ AdjustStarredVisualOrder(original_entry.parent_group_id,
+ original_entry.visual_order, -1);
+ // Make room for new location by shifting everything up from the new
+ // location.
+ AdjustStarredVisualOrder(original_entry.parent_group_id,
+ entry->visual_order, 1);
+ }
+
+ // And update title, parent and visual order.
+ UpdateStarredEntryRow(id, entry->title, entry->parent_group_id,
+ entry->visual_order, entry->date_group_modified);
+ entry->url_id = original_entry.url_id;
+ entry->date_added = original_entry.date_added;
+ entry->id = original_entry.id;
+ if (check_starred_integrity_on_mutation_)
+ CheckStarredIntegrity();
+ return true;
+}
+
+bool StarredURLDatabase::GetStarredEntries(
+ UIStarID parent_group_id,
+ std::vector<StarredEntry>* entries) {
+ DCHECK(entries);
+ std::string sql = "SELECT ";
+ sql.append(kHistoryStarFields);
+ sql.append("FROM starred LEFT JOIN urls ON starred.url_id = urls.id ");
+ if (parent_group_id)
+ sql += "WHERE starred.parent_id = ? ";
+ sql += "ORDER BY parent_id, visual_order";
+
+ SQLStatement s;
+ if (s.prepare(GetDB(), sql.c_str()) != SQLITE_OK) {
+ NOTREACHED() << "Statement prepare failed";
+ return false;
+ }
+ if (parent_group_id)
+ s.bind_int64(0, parent_group_id);
+
+ history::StarredEntry entry;
+ while (s.step() == SQLITE_ROW) {
+ FillInStarredEntry(&s, &entry);
+ // Reset the url for non-url types. This is needed as we're reusing the
+ // same entry for the loop.
+ if (entry.type != history::StarredEntry::URL)
+ entry.url = GURL();
+ entries->push_back(entry);
+ }
+ return true;
+}
+
+bool StarredURLDatabase::UpdateStarredEntryRow(StarID star_id,
+ const std::wstring& title,
+ UIStarID parent_group_id,
+ int visual_order,
+ Time date_modified) {
+ DCHECK(star_id && visual_order >= 0);
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "UPDATE starred SET title=?, parent_id=?, visual_order=?, "
+ "date_modified=? WHERE id=?");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_wstring(0, title);
+ statement->bind_int64(1, parent_group_id);
+ statement->bind_int(2, visual_order);
+ statement->bind_int64(3, date_modified.ToInternalValue());
+ statement->bind_int64(4, star_id);
+ return statement->step() == SQLITE_DONE;
+}
+
+bool StarredURLDatabase::AdjustStarredVisualOrder(UIStarID parent_group_id,
+ int start_visual_order,
+ int delta) {
+ DCHECK(parent_group_id && start_visual_order >= 0);
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "UPDATE starred SET visual_order=visual_order+? "
+ "WHERE parent_id=? AND visual_order >= ?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int(0, delta);
+ statement->bind_int64(1, parent_group_id);
+ statement->bind_int(2, start_visual_order);
+ return statement->step() == SQLITE_DONE;
+}
+
+StarID StarredURLDatabase::CreateStarredEntryRow(URLID url_id,
+ UIStarID group_id,
+ UIStarID parent_group_id,
+ const std::wstring& title,
+ const Time& date_added,
+ int visual_order,
+ StarredEntry::Type type) {
+ DCHECK(visual_order >= 0 &&
+ (type != history::StarredEntry::URL || url_id));
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "INSERT INTO starred "
+ "(type, url_id, group_id, title, date_added, visual_order, parent_id, "
+ "date_modified) VALUES (?,?,?,?,?,?,?,?)");
+ if (!statement.is_valid())
+ return 0;
+
+ switch (type) {
+ case history::StarredEntry::URL:
+ statement->bind_int(0, 0);
+ break;
+ case history::StarredEntry::BOOKMARK_BAR:
+ statement->bind_int(0, 1);
+ break;
+ case history::StarredEntry::USER_GROUP:
+ statement->bind_int(0, 2);
+ break;
+ case history::StarredEntry::OTHER:
+ statement->bind_int(0, 3);
+ break;
+ default:
+ NOTREACHED();
+ }
+ statement->bind_int64(1, url_id);
+ statement->bind_int64(2, group_id);
+ statement->bind_wstring(3, title);
+ statement->bind_int64(4, date_added.ToInternalValue());
+ statement->bind_int(5, visual_order);
+ statement->bind_int64(6, parent_group_id);
+ statement->bind_int64(7, Time().ToInternalValue());
+ if (statement->step() == SQLITE_DONE)
+ return sqlite3_last_insert_rowid(GetDB());
+ return 0;
+}
+
+bool StarredURLDatabase::DeleteStarredEntryRow(StarID star_id) {
+ if (check_starred_integrity_on_mutation_)
+ DCHECK(star_id && star_id != HistoryService::kBookmarkBarID);
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "DELETE FROM starred WHERE id=?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, star_id);
+ return statement->step() == SQLITE_DONE;
+}
+
+bool StarredURLDatabase::GetStarredEntry(StarID star_id, StarredEntry* entry) {
+ DCHECK(entry && star_id);
+ SQLITE_UNIQUE_STATEMENT(s, GetStatementCache(),
+ "SELECT" STAR_FIELDS "FROM starred LEFT JOIN urls ON "
+ "starred.url_id = urls.id WHERE starred.id=?");
+ if (!s.is_valid())
+ return false;
+
+ s->bind_int64(0, star_id);
+
+ if (s->step() == SQLITE_ROW) {
+ FillInStarredEntry(s.statement(), entry);
+ return true;
+ }
+ return false;
+}
+
+StarID StarredURLDatabase::CreateStarredEntry(StarredEntry* entry) {
+ entry->id = 0; // Ensure 0 for failure case.
+
+ // Adjust the visual order when we are inserting it somewhere.
+ if (entry->parent_group_id)
+ AdjustStarredVisualOrder(entry->parent_group_id, entry->visual_order, 1);
+
+ // Insert the new entry.
+ switch (entry->type) {
+ case StarredEntry::USER_GROUP:
+ entry->id = CreateStarredEntryRow(0, entry->group_id,
+ entry->parent_group_id, entry->title, entry->date_added,
+ entry->visual_order, entry->type);
+ break;
+
+ case StarredEntry::URL: {
+ // Get the row for this URL.
+ URLRow url_row;
+ if (!GetRowForURL(entry->url, &url_row)) {
+ // Create a new URL row for this entry.
+ url_row = URLRow(entry->url);
+ url_row.set_title(entry->title);
+ url_row.set_hidden(false);
+ entry->url_id = this->AddURL(url_row);
+ } else {
+ // Update the existing row for this URL.
+ if (url_row.starred()) {
+ DCHECK(false) << "We are starring an already-starred URL";
+ return 0;
+ }
+ entry->url_id = url_row.id(); // The caller doesn't have to set this.
+ }
+
+ // Create the star entry referring to the URL row.
+ entry->id = CreateStarredEntryRow(entry->url_id, entry->group_id,
+ entry->parent_group_id, entry->title, entry->date_added,
+ entry->visual_order, entry->type);
+
+ // Update the URL row to refer to this new starred entry.
+ url_row.set_star_id(entry->id);
+ UpdateURLRow(entry->url_id, url_row);
+ break;
+ }
+
+ default:
+ NOTREACHED();
+ break;
+ }
+ if (check_starred_integrity_on_mutation_)
+ CheckStarredIntegrity();
+ return entry->id;
+}
+
+void StarredURLDatabase::GetMostRecentStarredEntries(
+ int max_count,
+ std::vector<StarredEntry>* entries) {
+ DCHECK(max_count && entries);
+ SQLITE_UNIQUE_STATEMENT(s, GetStatementCache(),
+ "SELECT" STAR_FIELDS "FROM starred "
+ "LEFT JOIN urls ON starred.url_id = urls.id "
+ "WHERE starred.type=0 ORDER BY starred.date_added DESC, "
+ "starred.id DESC " // This is for testing, when the dates are
+ // typically the same.
+ "LIMIT ?");
+ if (!s.is_valid())
+ return;
+ s->bind_int(0, max_count);
+
+ while (s->step() == SQLITE_ROW) {
+ history::StarredEntry entry;
+ FillInStarredEntry(s.statement(), &entry);
+ entries->push_back(entry);
+ }
+}
+
+void StarredURLDatabase::GetURLsForTitlesMatching(const std::wstring& query,
+ std::set<URLID>* ids) {
+ QueryParser parser;
+ ScopedVector<QueryNode> nodes;
+ parser.ParseQuery(query, &nodes.get());
+ if (nodes.empty())
+ return;
+
+ SQLITE_UNIQUE_STATEMENT(s, GetStatementCache(),
+ "SELECT url_id, title FROM starred WHERE type=?");
+ if (!s.is_valid())
+ return;
+
+ s->bind_int(0, static_cast<int>(StarredEntry::URL));
+ while (s->step() == SQLITE_ROW) {
+ if (parser.DoesQueryMatch(s->column_string16(1), nodes.get()))
+ ids->insert(s->column_int64(0));
+ }
+}
+
+bool StarredURLDatabase::UpdateURLIDForStar(StarID star_id, URLID new_url_id) {
+ SQLITE_UNIQUE_STATEMENT(s, GetStatementCache(),
+ "UPDATE starred SET url_id=? WHERE id=?");
+ if (!s.is_valid())
+ return false;
+ s->bind_int64(0, new_url_id);
+ s->bind_int64(1, star_id);
+ return s->step() == SQLITE_DONE;
+}
+
+void StarredURLDatabase::DeleteStarredEntryImpl(
+ StarID star_id,
+ std::set<GURL>* unstarred_urls,
+ std::vector<StarredEntry>* deleted_entries) {
+
+ StarredEntry deleted_entry;
+ if (!GetStarredEntry(star_id, &deleted_entry))
+ return; // Couldn't find information on the entry.
+
+ if (deleted_entry.type == StarredEntry::BOOKMARK_BAR ||
+ deleted_entry.type == StarredEntry::OTHER) {
+ return;
+ }
+
+ if (deleted_entry.parent_group_id != 0) {
+ // This entry has a parent. All siblings after this entry need to be shifted
+ // down by 1.
+ AdjustStarredVisualOrder(deleted_entry.parent_group_id,
+ deleted_entry.visual_order, -1);
+ }
+
+ deleted_entries->push_back(deleted_entry);
+
+ switch (deleted_entry.type) {
+ case StarredEntry::URL: {
+ unstarred_urls->insert(deleted_entry.url);
+
+ // Need to unmark the starred flag in the URL database.
+ URLRow url_row;
+ if (GetURLRow(deleted_entry.url_id, &url_row)) {
+ url_row.set_star_id(0);
+ UpdateURLRow(deleted_entry.url_id, url_row);
+ }
+ break;
+ }
+
+ case StarredEntry::USER_GROUP: {
+ // Need to delete all the children of the group. Use the db version which
+ // avoids shifting the visual order.
+ std::vector<StarredEntry> descendants;
+ GetStarredEntries(deleted_entry.group_id, &descendants);
+ for (std::vector<StarredEntry>::iterator i =
+ descendants.begin(); i != descendants.end(); ++i) {
+ // Recurse down into the child.
+ DeleteStarredEntryImpl(i->id, unstarred_urls, deleted_entries);
+ }
+ break;
+ }
+
+ default:
+ NOTREACHED();
+ break;
+ }
+ DeleteStarredEntryRow(star_id);
+}
+
+UIStarID StarredURLDatabase::GetMaxGroupID() {
+ SQLStatement max_group_id_statement;
+ if (max_group_id_statement.prepare(GetDB(),
+ "SELECT MAX(group_id) FROM starred") != SQLITE_OK) {
+ NOTREACHED();
+ return 0;
+ }
+ if (max_group_id_statement.step() != SQLITE_ROW) {
+ NOTREACHED();
+ return 0;
+ }
+ return max_group_id_statement.column_int64(0);
+}
+
+bool StarredURLDatabase::BuildStarNodes(
+ std::set<StarredURLDatabase::StarredNode*>* roots,
+ std::set<StarID>* groups_with_duplicate_ids,
+ std::set<StarredNode*>* unparented_urls,
+ std::set<StarID>* empty_url_ids) {
+ std::vector<StarredEntry> star_entries;
+ if (!GetStarredEntries(0, &star_entries)) {
+ NOTREACHED() << "Unable to get bookmarks from database";
+ return false;
+ }
+
+ // Create the group/bookmark-bar/other nodes.
+ std::map<UIStarID, StarredNode*> group_id_to_node_map;
+ for (size_t i = 0; i < star_entries.size(); ++i) {
+ if (star_entries[i].type != StarredEntry::URL) {
+ if (group_id_to_node_map.find(star_entries[i].group_id) !=
+ group_id_to_node_map.end()) {
+ // There's already a group with this ID.
+ groups_with_duplicate_ids->insert(star_entries[i].id);
+ } else {
+ // Create the node and update the mapping.
+ StarredNode* node = new StarredNode(star_entries[i]);
+ group_id_to_node_map[star_entries[i].group_id] = node;
+ }
+ }
+ }
+
+ // Iterate again, creating nodes for URL bookmarks and parenting all
+ // bookmarks/folders. In addition populate the empty_url_ids with all entries
+ // of type URL that have an empty URL.
+ std::map<StarID, StarredNode*> id_to_node_map;
+ for (size_t i = 0; i < star_entries.size(); ++i) {
+ if (star_entries[i].type == StarredEntry::URL) {
+ if (star_entries[i].url.is_empty()) {
+ empty_url_ids->insert(star_entries[i].id);
+ } else if (!star_entries[i].parent_group_id ||
+ group_id_to_node_map.find(star_entries[i].parent_group_id) ==
+ group_id_to_node_map.end()) {
+ // This entry has no parent, or we couldn't find the parent.
+ StarredNode* node = new StarredNode(star_entries[i]);
+ unparented_urls->insert(node);
+ } else {
+ // Add the node to its parent.
+ StarredNode* parent =
+ group_id_to_node_map[star_entries[i].parent_group_id];
+ StarredNode* node = new StarredNode(star_entries[i]);
+ parent->Add(parent->GetChildCount(), node);
+ }
+ } else if (groups_with_duplicate_ids->find(star_entries[i].id) ==
+ groups_with_duplicate_ids->end()) {
+ // The entry is a group (or bookmark bar/other node) that isn't
+ // marked as a duplicate.
+ if (!star_entries[i].parent_group_id ||
+ group_id_to_node_map.find(star_entries[i].parent_group_id) ==
+ group_id_to_node_map.end()) {
+ // Entry has no parent, or the parent wasn't found.
+ roots->insert(group_id_to_node_map[star_entries[i].group_id]);
+ } else {
+ // Parent the group node.
+ StarredNode* parent =
+ group_id_to_node_map[star_entries[i].parent_group_id];
+ StarredNode* node = group_id_to_node_map[star_entries[i].group_id];
+ if (!node->HasAncestor(parent) && !parent->HasAncestor(node)) {
+ parent->Add(parent->GetChildCount(), node);
+ } else{
+ // The node has a cycle. Add it to the list of roots so the cycle is
+ // broken.
+ roots->insert(node);
+ }
+ }
+ }
+ }
+ return true;
+}
+
+StarredURLDatabase::StarredNode* StarredURLDatabase::GetNodeByType(
+ const std::set<StarredURLDatabase::StarredNode*>& nodes,
+ StarredEntry::Type type) {
+ for (std::set<StarredNode*>::const_iterator i = nodes.begin();
+ i != nodes.end(); ++i) {
+ if ((*i)->value.type == type)
+ return *i;
+ }
+ return NULL;
+}
+
+bool StarredURLDatabase::EnsureVisualOrder(
+ StarredURLDatabase::StarredNode* node) {
+ for (int i = 0; i < node->GetChildCount(); ++i) {
+ if (node->GetChild(i)->value.visual_order != i) {
+ StarredEntry& entry = node->GetChild(i)->value;
+ entry.visual_order = i;
+ LOG(WARNING) << "Bookmark visual order is wrong";
+ if (!UpdateStarredEntryRow(entry.id, entry.title, entry.parent_group_id,
+ i, entry.date_group_modified)) {
+ NOTREACHED() << "Unable to update visual order";
+ return false;
+ }
+ }
+ if (!EnsureVisualOrder(node->GetChild(i)))
+ return false;
+ }
+ return true;
+}
+
+bool StarredURLDatabase::EnsureStarredIntegrityImpl(
+ std::set<StarredURLDatabase::StarredNode*>* roots,
+ const std::set<StarID>& groups_with_duplicate_ids,
+ std::set<StarredNode*>* unparented_urls,
+ const std::set<StarID>& empty_url_ids) {
+ // Make sure the bookmark bar entry exists.
+ StarredNode* bookmark_node =
+ GetNodeByType(*roots, StarredEntry::BOOKMARK_BAR);
+ if (!bookmark_node) {
+ LOG(WARNING) << "No bookmark bar folder in database";
+ // If there is no bookmark bar entry in the db things are really
+ // screwed. The UI assumes the bookmark bar entry has a particular
+ // ID which is hard to enforce when creating a new entry. As this
+ // shouldn't happen, we take the drastic route of recreating the
+ // entire table.
+ return RecreateStarredEntries();
+ }
+
+ // Make sure the other node exists.
+ StarredNode* other_node = GetNodeByType(*roots, StarredEntry::OTHER);
+ if (!other_node) {
+ LOG(WARNING) << "No bookmark other folder in database";
+ StarredEntry entry;
+ entry.group_id = GetMaxGroupID() + 1;
+ if (entry.group_id == 1) {
+ NOTREACHED() << "Unable to get new id for other bookmarks folder";
+ return false;
+ }
+ entry.id = CreateStarredEntryRow(
+ 0, entry.group_id, 0, L"other", Time::Now(), 0,
+ history::StarredEntry::OTHER);
+ if (!entry.id) {
+ NOTREACHED() << "Unable to create other bookmarks folder";
+ return false;
+ }
+ entry.type = StarredEntry::OTHER;
+ StarredNode* other_node = new StarredNode(entry);
+ roots->insert(other_node);
+ }
+
+ // We could potentially make sure only one group with type
+ // BOOKMARK_BAR/OTHER, but history backend enforces this.
+
+ // Nuke any entries with no url.
+ for (std::set<StarID>::const_iterator i = empty_url_ids.begin();
+ i != empty_url_ids.end(); ++i) {
+ LOG(WARNING) << "Bookmark exists with no URL";
+ if (!DeleteStarredEntryRow(*i)) {
+ NOTREACHED() << "Unable to delete bookmark";
+ return false;
+ }
+ }
+
+ // Make sure the visual order of the nodes is correct.
+ for (std::set<StarredNode*>::const_iterator i = roots->begin();
+ i != roots->end(); ++i) {
+ if (!EnsureVisualOrder(*i))
+ return false;
+ }
+
+ // Move any unparented bookmarks to the bookmark bar.
+ {
+ std::set<StarredNode*>::iterator i = unparented_urls->begin();
+ while (i != unparented_urls->end()) {
+ LOG(WARNING) << "Bookmark not in a bookmark folder found";
+ if (!Move(*i, bookmark_node))
+ return false;
+ i = unparented_urls->erase(i);
+ }
+ }
+
+ // Nuke any groups with duplicate ids. A duplicate id means there are two
+ // folders in the starred table with the same group_id. We only keep the
+ // first folder, all other groups are removed.
+ for (std::set<StarID>::const_iterator i = groups_with_duplicate_ids.begin();
+ i != groups_with_duplicate_ids.end(); ++i) {
+ LOG(WARNING) << "Duplicate group id in bookmark database";
+ if (!DeleteStarredEntryRow(*i)) {
+ NOTREACHED() << "Unable to delete folder";
+ return false;
+ }
+ }
+
+ // Move unparented user groups back to the bookmark bar.
+ {
+ std::set<StarredNode*>::iterator i = roots->begin();
+ while (i != roots->end()) {
+ if ((*i)->value.type == StarredEntry::USER_GROUP) {
+ LOG(WARNING) << "Bookmark folder not on bookmark bar found";
+ if (!Move(*i, bookmark_node))
+ return false;
+ i = roots->erase(i);
+ } else {
+ ++i;
+ }
+ }
+ }
+
+ return true;
+}
+
+bool StarredURLDatabase::Move(StarredNode* source, StarredNode* new_parent) {
+ history::StarredEntry& entry = source->value;
+ entry.visual_order = new_parent->GetChildCount();
+ entry.parent_group_id = new_parent->value.group_id;
+ if (!UpdateStarredEntryRow(entry.id, entry.title,
+ entry.parent_group_id, entry.visual_order,
+ entry.date_group_modified)) {
+ NOTREACHED() << "Unable to move folder";
+ return false;
+ }
+ new_parent->Add(new_parent->GetChildCount(), source);
+ return true;
+}
+
+bool StarredURLDatabase::RecreateStarredEntries() {
+ if (sqlite3_exec(GetDB(), "DROP TABLE starred", NULL, NULL,
+ NULL) != SQLITE_OK) {
+ NOTREACHED() << "Unable to drop starred table";
+ return false;
+ }
+
+ if (!InitStarTable()) {
+ NOTREACHED() << "Unable to create starred table";
+ return false;
+ }
+
+ if (sqlite3_exec(GetDB(), "UPDATE urls SET starred_id=0", NULL, NULL,
+ NULL) != SQLITE_OK) {
+ NOTREACHED() << "Unable to mark all URLs as unstarred";
+ return false;
+ }
+ return true;
+}
+
+void StarredURLDatabase::CheckVisualOrder(StarredNode* node) {
+ for (int i = 0; i < node->GetChildCount(); ++i) {
+ DCHECK(i == node->GetChild(i)->value.visual_order);
+ CheckVisualOrder(node->GetChild(i));
+ }
+}
+
+void StarredURLDatabase::CheckStarredIntegrity() {
+#ifndef NDEBUG
+ std::set<StarredNode*> roots;
+ std::set<StarID> groups_with_duplicate_ids;
+ std::set<StarredNode*> unparented_urls;
+ std::set<StarID> empty_url_ids;
+
+ if (!BuildStarNodes(&roots, &groups_with_duplicate_ids, &unparented_urls,
+ &empty_url_ids)) {
+ return;
+ }
+
+ // There must be root and bookmark bar nodes.
+ if (!GetNodeByType(roots, StarredEntry::BOOKMARK_BAR))
+ NOTREACHED() << "No bookmark bar folder in starred";
+ else
+ CheckVisualOrder(GetNodeByType(roots, StarredEntry::BOOKMARK_BAR));
+
+ if (!GetNodeByType(roots, StarredEntry::OTHER))
+ NOTREACHED() << "No other bookmarks folder in starred";
+ else
+ CheckVisualOrder(GetNodeByType(roots, StarredEntry::OTHER));
+
+ // The only roots should be the bookmark bar and other nodes. Also count how
+ // many bookmark bar and other folders exists.
+ int bookmark_bar_count = 0;
+ int other_folder_count = 0;
+ for (std::set<StarredNode*>::const_iterator i = roots.begin();
+ i != roots.end(); ++i) {
+ if ((*i)->value.type == StarredEntry::USER_GROUP)
+ NOTREACHED() << "Bookmark folder not in bookmark bar or other folders";
+ else if ((*i)->value.type == StarredEntry::BOOKMARK_BAR)
+ bookmark_bar_count++;
+ else if ((*i)->value.type == StarredEntry::OTHER)
+ other_folder_count++;
+ }
+ DCHECK(bookmark_bar_count == 1) << "Should be only one bookmark bar entry";
+ DCHECK(other_folder_count == 1) << "Should be only one other folder entry";
+
+ // There shouldn't be any folders with the same group id.
+ if (!groups_with_duplicate_ids.empty())
+ NOTREACHED() << "Bookmark folder with duplicate ids exist";
+
+ // And all URLs should be parented.
+ if (!unparented_urls.empty())
+ NOTREACHED() << "Bookmarks not on the bookmark/'other folder' exist";
+
+ if (!empty_url_ids.empty())
+ NOTREACHED() << "Bookmarks with no corresponding URL exist";
+
+ STLDeleteElements(&roots);
+ STLDeleteElements(&unparented_urls);
+#endif NDEBUG
+}
+
+} // namespace history
diff --git a/chrome/browser/history/starred_url_database.h b/chrome/browser/history/starred_url_database.h
new file mode 100644
index 0000000..d58571d
--- /dev/null
+++ b/chrome/browser/history/starred_url_database.h
@@ -0,0 +1,289 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_STARRED_URL_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_STARRED_URL_DATABASE_H__
+
+#include <map>
+#include <set>
+
+#include "base/basictypes.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/views/tree_node_model.h"
+#include "testing/gtest/include/gtest/gtest_prod.h"
+
+struct sqlite3;
+class SqliteStatementCache;
+
+namespace history {
+
+// Encapsulates a URL database plus starred information.
+//
+// WARNING: many of the following methods allow you to update, delete or
+// insert starred entries specifying a visual order. These methods do NOT
+// adjust the visual order of surrounding entries. You must explicitly do
+// it yourself using AdjustStarredVisualOrder as appropriate.
+class StarredURLDatabase : public URLDatabase {
+ public:
+ // Must call InitStarTable() AND any additional init functions provided by
+ // URLDatabase before using this class' functions.
+ StarredURLDatabase();
+ virtual ~StarredURLDatabase();
+
+ // Returns the id of the starred entry. This does NOT return entry.id,
+ // rather the database is queried for the id based on entry.url or
+ // entry.group_id.
+ StarID GetStarIDForEntry(const StarredEntry& entry);
+
+ // Returns the internal star ID for the externally-generated group ID.
+ StarID GetStarIDForGroupID(UIStarID group_id);
+
+ // Gets the details for the specified star entry in entry.
+ bool GetStarredEntry(StarID star_id, StarredEntry* entry);
+
+ // Creates a starred entry with the requested information. The structure will
+ // be updated with the ID of the newly created entry. The URL table will be
+ // updated to point to the entry. The URL row will be created if it doesn't
+ // exist.
+ //
+ // We currently only support one entry per URL. This URL should not already be
+ // starred when calling this function or it will fail and will return 0.
+ StarID CreateStarredEntry(StarredEntry* entry);
+
+ // Returns starred entries.
+ // This returns three different result types:
+ // only_on_bookmark_bar == true: only those entries on the bookmark bar are
+ // returned.
+ // only_on_bookmark_bar == false and parent_id == 0: all starred
+ // entries/groups.
+ // otherwise only the direct children (groups and entries) of parent_id are
+ // returned.
+ bool GetStarredEntries(UIStarID parent_group_id,
+ std::vector<StarredEntry>* entries);
+
+ // Deletes a starred entry. This adjusts the visual order of all siblings
+ // after the entry. The deleted entry(s) will be *appended* to
+ // |*deleted_entries| (so delete can be called more than once with the same
+ // list) and deleted URLs will additionally be added to the set.
+ //
+ // This function invokes DeleteStarredEntryImpl to do the actual work, which
+ // recurses.
+ void DeleteStarredEntry(StarID star_id,
+ std::set<GURL>* unstarred_urls,
+ std::vector<StarredEntry>* deleted_entries);
+
+ // Implementation to update the database for UpdateStarredEntry. Returns true
+ // on success. If successful, the parent_id, url_id, id and date_added fields
+ // of the entry are reset from that of the database.
+ bool UpdateStarredEntry(StarredEntry* entry);
+
+ // Gets up to max_count starred entries of type URL adding them to entries.
+ // The results are ordered by date added in descending order (most recent
+ // first).
+ void GetMostRecentStarredEntries(int max_count,
+ std::vector<StarredEntry>* entries);
+
+ // Gets the URLIDs for all starred entries of type URL whose title matches
+ // the specified query.
+ void GetURLsForTitlesMatching(const std::wstring& query,
+ std::set<URLID>* ids);
+
+ // Returns true if the starred table is in a sane state. If false, the starred
+ // table is likely corrupt and shouldn't be used.
+ bool is_starred_valid() const { return is_starred_valid_; }
+
+ // Updates the URL ID for the given starred entry. This is used by the expirer
+ // when URL IDs change. It can't use UpdateStarredEntry since that doesn't
+ // set the URLID, and it does a bunch of other logic that we don't need.
+ bool UpdateURLIDForStar(StarID star_id, URLID new_url_id);
+
+ protected:
+ // The unit tests poke our innards.
+ friend class HistoryTest;
+ FRIEND_TEST(HistoryTest, CreateStarGroup);
+
+ // Returns the database and statement cache for the functions in this
+ // interface. The decendent of this class implements these functions to
+ // return its objects.
+ virtual sqlite3* GetDB() = 0;
+ virtual SqliteStatementCache& GetStatementCache() = 0;
+
+ // Creates the starred tables if necessary.
+ bool InitStarTable();
+
+ // Makes sure the starred table is in a sane state. This does the following:
+ // . Makes sure there is a bookmark bar and other nodes. If no bookmark bar
+ // node is found, the table is dropped and recreated.
+ // . Removes any bookmarks with no URL. This can happen if a URL is removed
+ // from the urls table without updating the starred table correctly.
+ // . Makes sure the visual order of all nodes is correct.
+ // . Moves all bookmarks and folders that are not descendants of the bookmark
+ // bar or other folders to the bookmark bar.
+ // . Makes sure there isn't a cycle in the folders. A cycle means some folder
+ // has as its parent one of its children.
+ //
+ // This returns false if the starred table is in a bad state and couldn't
+ // be fixed, true otherwise.
+ //
+ // This should be invoked after migration.
+ bool EnsureStarredIntegrity();
+
+ // Creates a starred entry with the specified parameters in the database.
+ // Returns the newly created id, or 0 on failure.
+ //
+ // WARNING: Does not update the visual order.
+ StarID CreateStarredEntryRow(URLID url_id,
+ UIStarID group_id,
+ UIStarID parent_group_id,
+ const std::wstring& title,
+ const Time& date_added,
+ int visual_order,
+ StarredEntry::Type type);
+
+ // Sets the title, parent_id, parent_group_id, visual_order and date_modifed
+ // of the specified star entry.
+ //
+ // WARNING: Does not update the visual order.
+ bool UpdateStarredEntryRow(StarID star_id,
+ const std::wstring& title,
+ UIStarID parent_group_id,
+ int visual_order,
+ Time date_modified);
+
+ // Adjusts the visual order of all children of parent_group_id with a
+ // visual_order >= start_visual_order by delta. For example,
+ // AdjustStarredVisualOrder(10, 0, 1) increments the visual order all children
+ // of group 10 with a visual order >= 0 by 1.
+ bool AdjustStarredVisualOrder(UIStarID parent_group_id,
+ int start_visual_order,
+ int delta);
+
+ // Deletes the entry from the starred database base on the starred id (NOT
+ // the url id).
+ //
+ // WARNING: Does not update the visual order.
+ bool DeleteStarredEntryRow(StarID star_id);
+
+ // Should the integrity of the starred table be checked on every mutation?
+ // Default is true. Set to false when running tests that need to allow the
+ // table to be in an invalid state while testing.
+ bool check_starred_integrity_on_mutation_;
+
+ private:
+ // Used when checking integrity of starred table.
+ typedef ChromeViews::TreeNodeWithValue<history::StarredEntry> StarredNode;
+
+ // Implementation of DeleteStarredEntryImpl.
+ void DeleteStarredEntryImpl(StarID star_id,
+ std::set<GURL>* unstarred_urls,
+ std::vector<StarredEntry>* deleted_entries);
+
+ // Returns the max group id, or 0 if there is an error.
+ UIStarID GetMaxGroupID();
+
+ // Gets all the bookmarks and folders creating a StarredNode for each
+ // bookmark and folder. On success all the root nodes (bookmark bar node,
+ // other folder node, folders with no parent or folders with a parent that
+ // would make a cycle) are added to roots.
+ //
+ // If a group_id occurs more than once, all but the first ones id is added to
+ // groups_with_duplicate_ids.
+ //
+ // All bookmarks not on the bookmark bar/other folder are added to
+ // unparented_urls.
+ //
+ // It's up to the caller to delete the nodes returned in roots and
+ // unparented_urls.
+ //
+ // This is used during integrity enforcing/checking of the starred table.
+ bool BuildStarNodes(
+ std::set<StarredNode*>* roots,
+ std::set<StarID>* groups_with_duplicate_ids,
+ std::set<StarredNode*>* unparented_urls,
+ std::set<StarID>* empty_url_ids);
+
+ // Sets the visual order of all of node's children match the order in |node|.
+ // If the order differs, the database is updated. Returns false if the order
+ // differed and the db couldn't be updated.
+ bool EnsureVisualOrder(StarredNode* node);
+
+ // Returns the first node in nodes with the specified type, or null if there
+ // is not a node with the specified type.
+ StarredNode* GetNodeByType(
+ const std::set<StarredNode*>& nodes,
+ StarredEntry::Type type);
+
+ // Implementation for setting starred integrity. See description of
+ // EnsureStarredIntegrity for the details of what this does.
+ //
+ // All entries in roots that are not the bookmark bar and other node are
+ // moved to be children of the bookmark bar node. Similarly all nodes
+ // in unparented_urls are moved to be children of the bookmark bar.
+ //
+ // Returns true on success, false if the starred table is in a bad state and
+ // couldn't be repaired.
+ bool EnsureStarredIntegrityImpl(
+ std::set<StarredNode*>* roots,
+ const std::set<StarID>& groups_with_duplicate_ids,
+ std::set<StarredNode*>* unparented_urls,
+ const std::set<StarID>& empty_url_ids);
+
+ // Resets the visual order and parent_group_id of source's StarredEntry
+ // and adds it to the end of new_parent's children.
+ //
+ // This is used if the starred table is an unexpected state and an entry
+ // needs to be moved.
+ bool Move(StarredNode* source, StarredNode* new_parent);
+
+ // Drops and recreates the starred table as well as marking all URLs as
+ // unstarred. This is used as a last resort if the bookmarks table is
+ // totally corrupt.
+ bool RecreateStarredEntries();
+
+ // Iterates through the children of node make sure the visual order matches
+ // the order in node's children. DCHECKs if the order differs. This recurses.
+ void CheckVisualOrder(StarredNode* node);
+
+ // Makes sure the starred table is in a sane state, if not this DCHECKs.
+ // This is invoked internally after any mutation to the starred table.
+ //
+ // This is a no-op in release builds.
+ void CheckStarredIntegrity();
+
+ // True if the starred table is valid. This is initialized in
+ // EnsureStarredIntegrityImpl.
+ bool is_starred_valid_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(StarredURLDatabase);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_STARRED_URL_DATABASE_H__
diff --git a/chrome/browser/history/starred_url_database_unittest.cc b/chrome/browser/history/starred_url_database_unittest.cc
new file mode 100644
index 0000000..aca91d9
--- /dev/null
+++ b/chrome/browser/history/starred_url_database_unittest.cc
@@ -0,0 +1,674 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <vector>
+
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/string_util.h"
+#include "chrome/browser/history/history.h"
+#include "chrome/browser/history/starred_url_database.h"
+#include "chrome/common/sqlite_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace history {
+
+class StarredURLDatabaseTest : public testing::Test,
+ public StarredURLDatabase {
+ public:
+ StarredURLDatabaseTest() : db_(NULL), statement_cache_(NULL) {
+ }
+
+ void AddPage(const GURL& url) {
+ URLRow row(url);
+ row.set_visit_count(1);
+ EXPECT_TRUE(AddURL(row));
+ }
+
+ StarredEntry GetStarredEntryByURL(const GURL& url) {
+ std::vector<StarredEntry> starred;
+ EXPECT_TRUE(GetStarredEntries(0, &starred));
+ for (std::vector<StarredEntry>::iterator i = starred.begin();
+ i != starred.end(); ++i) {
+ if (i->url == url)
+ return *i;
+ }
+ EXPECT_TRUE(false);
+ return StarredEntry();
+ }
+
+ void CompareEntryByID(const StarredEntry& entry) {
+ DCHECK(entry.id != 0);
+ StarredEntry db_value;
+ EXPECT_TRUE(GetStarredEntry(entry.id, &db_value));
+ EXPECT_EQ(entry.id, db_value.id);
+ EXPECT_TRUE(entry.title == db_value.title);
+ EXPECT_EQ(entry.date_added.ToTimeT(), db_value.date_added.ToTimeT());
+ EXPECT_EQ(entry.group_id, db_value.group_id);
+ EXPECT_EQ(entry.parent_group_id, db_value.parent_group_id);
+ EXPECT_EQ(entry.visual_order, db_value.visual_order);
+ EXPECT_EQ(entry.type, db_value.type);
+ EXPECT_EQ(entry.url_id, db_value.url_id);
+ if (entry.type == StarredEntry::URL)
+ EXPECT_TRUE(entry.url == db_value.url);
+ }
+
+ int GetStarredEntryCount() {
+ DCHECK(db_);
+ std::vector<StarredEntry> entries;
+ GetStarredEntries(0, &entries);
+ return static_cast<int>(entries.size());
+ }
+
+ bool GetURLStarred(const GURL& url) {
+ URLRow row;
+ if (GetRowForURL(url, &row))
+ return row.starred();
+ return false;
+ }
+
+ void VerifyInitialState() {
+ std::vector<StarredEntry> star_entries;
+ EXPECT_TRUE(GetStarredEntries(0, &star_entries));
+ EXPECT_EQ(2, star_entries.size());
+
+ int bb_index = -1;
+ size_t other_index = -1;
+ if (star_entries[0].type == history::StarredEntry::BOOKMARK_BAR) {
+ bb_index = 0;
+ other_index = 1;
+ } else {
+ bb_index = 1;
+ other_index = 0;
+ }
+ EXPECT_EQ(HistoryService::kBookmarkBarID, star_entries[bb_index].id);
+ EXPECT_EQ(HistoryService::kBookmarkBarID, star_entries[bb_index].group_id);
+ EXPECT_EQ(0, star_entries[bb_index].parent_group_id);
+ EXPECT_EQ(StarredEntry::BOOKMARK_BAR, star_entries[bb_index].type);
+
+ EXPECT_TRUE(star_entries[other_index].id != HistoryService::kBookmarkBarID
+ && star_entries[other_index].id != 0);
+ EXPECT_TRUE(star_entries[other_index].group_id !=
+ HistoryService::kBookmarkBarID &&
+ star_entries[other_index].group_id != 0);
+ EXPECT_EQ(0, star_entries[other_index].parent_group_id);
+ EXPECT_EQ(StarredEntry::OTHER, star_entries[other_index].type);
+ }
+
+ private:
+ // Test setup.
+ void SetUp() {
+ PathService::Get(base::DIR_TEMP, &db_file_);
+ db_file_.push_back(file_util::kPathSeparator);
+ db_file_.append(L"VisitTest.db");
+ file_util::Delete(db_file_, false);
+
+ EXPECT_EQ(SQLITE_OK, sqlite3_open(WideToUTF8(db_file_).c_str(), &db_));
+ statement_cache_ = new SqliteStatementCache(db_);
+
+ // Initialize the tables for this test.
+ CreateURLTable(false);
+ CreateMainURLIndex();
+ InitStarTable();
+ EnsureStarredIntegrity();
+ }
+ void TearDown() {
+ delete statement_cache_;
+ sqlite3_close(db_);
+ file_util::Delete(db_file_, false);
+ }
+
+ // Provided for URL/StarredURLDatabase.
+ virtual sqlite3* GetDB() {
+ return db_;
+ }
+ virtual SqliteStatementCache& GetStatementCache() {
+ return *statement_cache_;
+ }
+
+ std::wstring db_file_;
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+};
+
+//-----------------------------------------------------------------------------
+
+TEST_F(StarredURLDatabaseTest, CreateStarredEntryUnstarred) {
+ StarredEntry entry;
+ entry.title = L"blah";
+ entry.date_added = Time::Now();
+ entry.url = GURL("http://www.google.com");
+ entry.parent_group_id = HistoryService::kBookmarkBarID;
+
+ StarID id = CreateStarredEntry(&entry);
+
+ EXPECT_NE(0, id);
+ EXPECT_EQ(id, entry.id);
+ CompareEntryByID(entry);
+}
+
+TEST_F(StarredURLDatabaseTest, UpdateStarredEntry) {
+ const int initial_count = GetStarredEntryCount();
+ StarredEntry entry;
+ entry.title = L"blah";
+ entry.date_added = Time::Now();
+ entry.url = GURL("http://www.google.com");
+ entry.parent_group_id = HistoryService::kBookmarkBarID;
+
+ StarID id = CreateStarredEntry(&entry);
+ EXPECT_NE(0, id);
+ EXPECT_EQ(id, entry.id);
+ CompareEntryByID(entry);
+
+ // Now invoke create again, as the URL hasn't changed this should just
+ // update the title.
+ EXPECT_TRUE(UpdateStarredEntry(&entry));
+ CompareEntryByID(entry);
+
+ // There should only be two entries (one for the url we created, one for the
+ // bookmark bar).
+ EXPECT_EQ(initial_count + 1, GetStarredEntryCount());
+}
+
+TEST_F(StarredURLDatabaseTest, DeleteStarredGroup) {
+ const int initial_count = GetStarredEntryCount();
+ StarredEntry entry;
+ entry.type = StarredEntry::USER_GROUP;
+ entry.title = L"blah";
+ entry.date_added = Time::Now();
+ entry.group_id = 10;
+ entry.parent_group_id = HistoryService::kBookmarkBarID;
+
+ StarID id = CreateStarredEntry(&entry);
+ EXPECT_NE(0, id);
+ EXPECT_NE(HistoryService::kBookmarkBarID, id);
+ CompareEntryByID(entry);
+
+ EXPECT_EQ(initial_count + 1, GetStarredEntryCount());
+
+ // Now delete it.
+ std::set<GURL> unstarred_urls;
+ std::vector<StarredEntry> deleted_entries;
+ DeleteStarredEntry(entry.id, &unstarred_urls, &deleted_entries);
+ ASSERT_EQ(0, unstarred_urls.size());
+ ASSERT_EQ(1, deleted_entries.size());
+ EXPECT_EQ(deleted_entries[0].id, id);
+
+ // Should only have the bookmark bar.
+ EXPECT_EQ(initial_count, GetStarredEntryCount());
+}
+
+TEST_F(StarredURLDatabaseTest, DeleteStarredGroupWithChildren) {
+ const int initial_count = GetStarredEntryCount();
+
+ StarredEntry entry;
+ entry.type = StarredEntry::USER_GROUP;
+ entry.title = L"blah";
+ entry.date_added = Time::Now();
+ entry.group_id = 10;
+ entry.parent_group_id = HistoryService::kBookmarkBarID;
+
+ StarID id = CreateStarredEntry(&entry);
+ EXPECT_NE(0, id);
+ EXPECT_NE(HistoryService::kBookmarkBarID, id);
+ CompareEntryByID(entry);
+
+ EXPECT_EQ(initial_count + 1, GetStarredEntryCount());
+
+ // Create a child of the group.
+ StarredEntry url_entry1;
+ url_entry1.parent_group_id = entry.group_id;
+ url_entry1.title = L"blah";
+ url_entry1.date_added = Time::Now();
+ url_entry1.url = GURL("http://www.google.com");
+ StarID url_id1 = CreateStarredEntry(&url_entry1);
+ EXPECT_NE(0, url_id1);
+ CompareEntryByID(url_entry1);
+
+ // Create a sibling of the group.
+ StarredEntry url_entry2;
+ url_entry2.parent_group_id = HistoryService::kBookmarkBarID;
+ url_entry2.title = L"blah";
+ url_entry2.date_added = Time::Now();
+ url_entry2.url = GURL("http://www.google2.com");
+ url_entry2.visual_order = 1;
+ StarID url_id2 = CreateStarredEntry(&url_entry2);
+ EXPECT_NE(0, url_id2);
+ CompareEntryByID(url_entry2);
+
+ EXPECT_EQ(initial_count + 3, GetStarredEntryCount());
+
+ // Now delete the group, which should delete url_entry1 and shift down the
+ // visual order of url_entry2.
+ std::set<GURL> unstarred_urls;
+ std::vector<StarredEntry> deleted_entries;
+ DeleteStarredEntry(entry.id, &unstarred_urls, &deleted_entries);
+ EXPECT_EQ(2, deleted_entries.size());
+ EXPECT_EQ(1, unstarred_urls.size());
+ EXPECT_TRUE((deleted_entries[0].id == id) || (deleted_entries[1].id == id));
+ EXPECT_TRUE((deleted_entries[0].id == url_id1) ||
+ (deleted_entries[1].id == url_id1));
+
+ url_entry2.visual_order--;
+ CompareEntryByID(url_entry2);
+
+ // Should have the bookmark bar, and url_entry2.
+ EXPECT_EQ(initial_count + 1, GetStarredEntryCount());
+}
+
+TEST_F(StarredURLDatabaseTest, InitialState) {
+ VerifyInitialState();
+}
+
+TEST_F(StarredURLDatabaseTest, CreateStarGroup) {
+ const int initial_count = GetStarredEntryCount();
+ std::wstring title(L"title");
+ Time time(Time::Now());
+ int visual_order = 0;
+
+ UIStarID ui_group_id = 10;
+ UIStarID ui_parent_group_id = HistoryService::kBookmarkBarID;
+
+ StarredEntry entry;
+ entry.type = StarredEntry::USER_GROUP;
+ entry.group_id = ui_group_id;
+ entry.parent_group_id = ui_parent_group_id;
+ entry.title = title;
+ entry.date_added = time;
+ entry.visual_order = visual_order;
+ StarID group_id = CreateStarredEntry(&entry);
+
+ EXPECT_NE(0, group_id);
+ EXPECT_NE(HistoryService::kBookmarkBarID, group_id);
+
+ EXPECT_EQ(group_id, GetStarIDForGroupID(ui_group_id));
+
+ StarredEntry group_entry;
+ EXPECT_TRUE(GetStarredEntry(group_id, &group_entry));
+ EXPECT_EQ(ui_group_id, group_entry.group_id);
+ EXPECT_EQ(ui_parent_group_id, group_entry.parent_group_id);
+ EXPECT_TRUE(group_entry.title == title);
+ // Don't use Time.== here as the conversion to time_t when writing to the db
+ // is lossy.
+ EXPECT_TRUE(group_entry.date_added.ToTimeT() == time.ToTimeT());
+ EXPECT_EQ(visual_order, group_entry.visual_order);
+
+ // Update the date modified.
+ Time date_modified = Time::Now();
+ entry.date_group_modified = date_modified;
+ UpdateStarredEntry(&group_entry);
+ EXPECT_TRUE(GetStarredEntry(group_id, &group_entry));
+ EXPECT_TRUE(entry.date_group_modified.ToTimeT() == date_modified.ToTimeT());
+}
+
+TEST_F(StarredURLDatabaseTest, UpdateStarredEntryChangeVisualOrder) {
+ const int initial_count = GetStarredEntryCount();
+ GURL url1(L"http://google.com/1");
+ GURL url2(L"http://google.com/2");
+
+ // Star url1, making it a child of the bookmark bar.
+ StarredEntry entry;
+ entry.url = url1;
+ entry.title = L"FOO";
+ entry.parent_group_id = HistoryService::kBookmarkBarID;
+ CreateStarredEntry(&entry);
+
+ // Make sure it was created correctly.
+ entry = GetStarredEntryByURL(url1);
+ EXPECT_EQ(GetRowForURL(url1, NULL), entry.url_id);
+ CompareEntryByID(entry);
+
+ // Star url2, making it a child of the bookmark bar, placing it after url2.
+ StarredEntry entry2;
+ entry2.url = url2;
+ entry2.visual_order = 1;
+ entry2.title = std::wstring();
+ entry2.parent_group_id = HistoryService::kBookmarkBarID;
+ CreateStarredEntry(&entry2);
+ entry2 = GetStarredEntryByURL(url2);
+ EXPECT_EQ(GetRowForURL(url2, NULL), entry2.url_id);
+ CompareEntryByID(entry2);
+
+ // Move url2 to be before url1.
+ entry2.visual_order = 0;
+ UpdateStarredEntry(&entry2);
+ CompareEntryByID(entry2);
+
+ // URL1's visual order should have shifted by one to accommodate URL2.
+ entry.visual_order = 1;
+ CompareEntryByID(entry);
+
+ // Now move URL1 to position 0, which will move url2 to position 1.
+ entry.visual_order = 0;
+ UpdateStarredEntry(&entry);
+ CompareEntryByID(entry);
+
+ entry2.visual_order = 1;
+ CompareEntryByID(entry2);
+
+ // Create a new group between url1 and url2.
+ StarredEntry g_entry;
+ g_entry.group_id = 10;
+ g_entry.parent_group_id = HistoryService::kBookmarkBarID;
+ g_entry.title = L"blah";
+ g_entry.visual_order = 1;
+ g_entry.date_added = Time::Now();
+ g_entry.type = StarredEntry::USER_GROUP;
+ g_entry.id = CreateStarredEntry(&g_entry);
+ EXPECT_NE(0, g_entry.id);
+ CompareEntryByID(g_entry);
+
+ // URL2 should have shifted to position 2.
+ entry2.visual_order = 2;
+ CompareEntryByID(entry2);
+
+ // Move url2 inside of group 1.
+ entry2.visual_order = 0;
+ entry2.parent_group_id = g_entry.group_id;
+ UpdateStarredEntry(&entry2);
+ CompareEntryByID(entry2);
+
+ // Move url1 to be a child of group 1 at position 0.
+ entry.parent_group_id = g_entry.group_id;
+ entry.visual_order = 0;
+ UpdateStarredEntry(&entry);
+ CompareEntryByID(entry);
+
+ // url2 should have moved to position 1.
+ entry2.visual_order = 1;
+ CompareEntryByID(entry2);
+
+ // And the group should have shifted to position 0.
+ g_entry.visual_order = 0;
+ CompareEntryByID(g_entry);
+
+ EXPECT_EQ(initial_count + 3, GetStarredEntryCount());
+
+ // Delete the group, which should unstar url1 and url2.
+ std::set<GURL> unstarred_urls;
+ std::vector<StarredEntry> deleted_entries;
+ DeleteStarredEntry(g_entry.id, &unstarred_urls, &deleted_entries);
+ EXPECT_EQ(initial_count, GetStarredEntryCount());
+ URLRow url_row;
+ GetRowForURL(url1, &url_row);
+ EXPECT_EQ(0, url_row.star_id());
+ GetRowForURL(url2, &url_row);
+ EXPECT_EQ(0, url_row.star_id());
+}
+
+TEST_F(StarredURLDatabaseTest, GetMostRecentStarredEntries) {
+ // Initially there shouldn't be any entries (only the bookmark bar, which
+ // isn't returned by GetMostRecentStarredEntries).
+ std::vector<StarredEntry> starred_entries;
+ GetMostRecentStarredEntries(10, &starred_entries);
+ EXPECT_EQ(0, starred_entries.size());
+
+ // Star url1.
+ GURL url1(L"http://google.com/1");
+ StarredEntry entry1;
+ entry1.type = StarredEntry::URL;
+ entry1.url = url1;
+ entry1.parent_group_id = HistoryService::kBookmarkBarID;
+ CreateStarredEntry(&entry1);
+
+ // Get the recent ones.
+ GetMostRecentStarredEntries(10, &starred_entries);
+ ASSERT_EQ(1, starred_entries.size());
+ EXPECT_TRUE(starred_entries[0].url == url1);
+
+ // Add url2 and star it.
+ GURL url2(L"http://google.com/2");
+ StarredEntry entry2;
+ entry2.type = StarredEntry::URL;
+ entry2.url = url2;
+ entry2.parent_group_id = HistoryService::kBookmarkBarID;
+ CreateStarredEntry(&entry2);
+
+ starred_entries.clear();
+ GetMostRecentStarredEntries(10, &starred_entries);
+ ASSERT_EQ(2, starred_entries.size());
+ EXPECT_TRUE(starred_entries[0].url == url2);
+ EXPECT_TRUE(starred_entries[1].url == url1);
+}
+
+TEST_F(StarredURLDatabaseTest, FixOrphanedGroup) {
+ check_starred_integrity_on_mutation_ = false;
+
+ const size_t initial_count = GetStarredEntryCount();
+
+ // Create a group that isn't parented to the other/bookmark folders.
+ StarredEntry g_entry;
+ g_entry.type = StarredEntry::USER_GROUP;
+ g_entry.parent_group_id = 100;
+ g_entry.visual_order = 10;
+ g_entry.group_id = 100;
+ CreateStarredEntry(&g_entry);
+
+ ASSERT_TRUE(EnsureStarredIntegrity());
+
+ // Make sure no new entries were added.
+ ASSERT_EQ(initial_count + 1, GetStarredEntryCount());
+
+ // Make sure the group was moved to the bookmark bar folder.
+ ASSERT_TRUE(GetStarredEntry(g_entry.id, &g_entry));
+ ASSERT_EQ(HistoryService::kBookmarkBarID, g_entry.parent_group_id);
+ ASSERT_EQ(0, g_entry.visual_order);
+}
+
+TEST_F(StarredURLDatabaseTest, FixOrphanedBookmarks) {
+ check_starred_integrity_on_mutation_ = false;
+
+ const size_t initial_count = GetStarredEntryCount();
+
+ // Create two bookmarks that aren't in a random folder no on the bookmark bar.
+ StarredEntry entry1;
+ entry1.parent_group_id = 100;
+ entry1.visual_order = 10;
+ entry1.url = GURL(L"http://google.com/1");
+ CreateStarredEntry(&entry1);
+
+ StarredEntry entry2;
+ entry2.parent_group_id = 101;
+ entry2.visual_order = 20;
+ entry2.url = GURL(L"http://google.com/2");
+ CreateStarredEntry(&entry2);
+
+ ASSERT_TRUE(EnsureStarredIntegrity());
+
+ // Make sure no new entries were added.
+ ASSERT_EQ(initial_count + 2, GetStarredEntryCount());
+
+ // Make sure the entries were moved to the bookmark bar and the visual order
+ // order was updated appropriately.
+ ASSERT_TRUE(GetStarredEntry(entry1.id, &entry1));
+ ASSERT_EQ(HistoryService::kBookmarkBarID, entry1.parent_group_id);
+
+ ASSERT_TRUE(GetStarredEntry(entry2.id, &entry2));
+ ASSERT_EQ(HistoryService::kBookmarkBarID, entry2.parent_group_id);
+ ASSERT_TRUE((entry1.visual_order == 0 && entry2.visual_order == 1) ||
+ (entry1.visual_order == 1 && entry2.visual_order == 0));
+}
+
+TEST_F(StarredURLDatabaseTest, FixGroupCycleDepth0) {
+ check_starred_integrity_on_mutation_ = false;
+
+ const size_t initial_count = GetStarredEntryCount();
+
+ // Create a group that is parented to itself.
+ StarredEntry entry1;
+ entry1.group_id = entry1.parent_group_id = 100;
+ entry1.visual_order = 10;
+ entry1.type = StarredEntry::USER_GROUP;
+ CreateStarredEntry(&entry1);
+
+ ASSERT_TRUE(EnsureStarredIntegrity());
+
+ // Make sure no new entries were added.
+ ASSERT_EQ(initial_count + 1, GetStarredEntryCount());
+
+ // Make sure the group were moved to the bookmark bar and the visual order
+ // order was updated appropriately.
+ ASSERT_TRUE(GetStarredEntry(entry1.id, &entry1));
+ ASSERT_EQ(HistoryService::kBookmarkBarID, entry1.parent_group_id);
+ ASSERT_EQ(0, entry1.visual_order);
+}
+
+TEST_F(StarredURLDatabaseTest, FixGroupCycleDepth1) {
+ check_starred_integrity_on_mutation_ = false;
+
+ const size_t initial_count = GetStarredEntryCount();
+
+ StarredEntry entry1;
+ entry1.group_id = 100;
+ entry1.parent_group_id = 101;
+ entry1.visual_order = 10;
+ entry1.type = StarredEntry::USER_GROUP;
+ CreateStarredEntry(&entry1);
+
+ StarredEntry entry2;
+ entry2.group_id = 101;
+ entry2.parent_group_id = 100;
+ entry2.visual_order = 11;
+ entry2.type = StarredEntry::USER_GROUP;
+ CreateStarredEntry(&entry2);
+
+ ASSERT_TRUE(EnsureStarredIntegrity());
+
+ // Make sure no new entries were added.
+ ASSERT_EQ(initial_count + 2, GetStarredEntryCount());
+
+ // Because the groups caused a cycle, entry1 is moved the bookmark bar, which
+ // breaks the cycle.
+ ASSERT_TRUE(GetStarredEntry(entry1.id, &entry1));
+ ASSERT_TRUE(GetStarredEntry(entry2.id, &entry2));
+ ASSERT_EQ(HistoryService::kBookmarkBarID, entry1.parent_group_id);
+ ASSERT_EQ(100, entry2.parent_group_id);
+ ASSERT_EQ(0, entry1.visual_order);
+ ASSERT_EQ(0, entry2.visual_order);
+}
+
+TEST_F(StarredURLDatabaseTest, FixVisualOrder) {
+ check_starred_integrity_on_mutation_ = false;
+
+ const size_t initial_count = GetStarredEntryCount();
+
+ // Star two urls.
+ StarredEntry entry1;
+ entry1.url = GURL(L"http://google.com/1");
+ entry1.parent_group_id = HistoryService::kBookmarkBarID;
+ entry1.visual_order = 5;
+ CreateStarredEntry(&entry1);
+
+ // Add url2 and star it.
+ StarredEntry entry2;
+ entry2.url = GURL(L"http://google.com/2");
+ entry2.parent_group_id = HistoryService::kBookmarkBarID;
+ entry2.visual_order = 10;
+ CreateStarredEntry(&entry2);
+
+ ASSERT_TRUE(EnsureStarredIntegrity());
+
+ // Make sure no new entries were added.
+ ASSERT_EQ(initial_count + 2, GetStarredEntryCount());
+
+ StarredEntry entry;
+ ASSERT_TRUE(GetStarredEntry(entry1.id, &entry));
+ entry1.visual_order = 0;
+ CompareEntryByID(entry1);
+
+ ASSERT_TRUE(GetStarredEntry(entry2.id, &entry));
+ entry2.visual_order = 1;
+ CompareEntryByID(entry2);
+}
+
+TEST_F(StarredURLDatabaseTest, RestoreOtherAndBookmarkBar) {
+ std::vector<StarredEntry> entries;
+ GetStarredEntries(0, &entries);
+
+ check_starred_integrity_on_mutation_ = false;
+
+ for (std::vector<StarredEntry>::iterator i = entries.begin();
+ i != entries.end(); ++i) {
+ if (i->type != StarredEntry::USER_GROUP) {
+ // Use this directly, otherwise we assert trying to delete other/bookmark
+ // bar.
+ DeleteStarredEntryRow(i->id);
+
+ ASSERT_TRUE(EnsureStarredIntegrity());
+
+ VerifyInitialState();
+ }
+ }
+}
+
+TEST_F(StarredURLDatabaseTest, FixDuplicateGroupIDs) {
+ check_starred_integrity_on_mutation_ = false;
+
+ const size_t initial_count = GetStarredEntryCount();
+
+ // Create two groups with the same group id.
+ StarredEntry entry1;
+ entry1.type = StarredEntry::USER_GROUP;
+ entry1.group_id = 10;
+ entry1.parent_group_id = HistoryService::kBookmarkBarID;
+ CreateStarredEntry(&entry1);
+ StarredEntry entry2 = entry1;
+ CreateStarredEntry(&entry2);
+
+ ASSERT_TRUE(EnsureStarredIntegrity());
+
+ // Make sure only one group exists.
+ ASSERT_EQ(initial_count + 1, GetStarredEntryCount());
+
+ StarredEntry entry;
+ ASSERT_TRUE(GetStarredEntry(entry1.id, &entry) ||
+ GetStarredEntry(entry2.id, &entry));
+}
+
+TEST_F(StarredURLDatabaseTest, RemoveStarredEntriesWithEmptyURL) {
+ const int initial_count = GetStarredEntryCount();
+
+ StarredEntry entry;
+ entry.url = GURL("http://google.com");
+ entry.title = L"FOO";
+ entry.parent_group_id = HistoryService::kBookmarkBarID;
+
+ ASSERT_NE(0, CreateStarredEntry(&entry));
+
+ // Remove the URL.
+ DeleteURLRow(entry.url_id);
+
+ // Fix up the table.
+ ASSERT_TRUE(EnsureStarredIntegrity());
+
+ // The entry we just created should have been nuked.
+ ASSERT_EQ(initial_count, GetStarredEntryCount());
+}
+
+} // namespace history
diff --git a/chrome/browser/history/text_database.cc b/chrome/browser/history/text_database.cc
new file mode 100644
index 0000000..0c1242f
--- /dev/null
+++ b/chrome/browser/history/text_database.cc
@@ -0,0 +1,412 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <limits>
+#include <set>
+
+#include "chrome/browser/history/text_database.h"
+
+#include "base/file_util.h"
+#include "base/logging.h"
+#include "base/string_util.h"
+#include "chrome/common/sqlite_utils.h"
+
+// There are two tables in each database, one full-text search (FTS) table which
+// indexes the contents and title of the pages. The other is a regular SQLITE
+// table which contains non-indexed information about the page. All columns of
+// a FTS table are indexed using the text search algorithm, which isn't what we
+// want for things like times. If this were in the FTS table, there would be
+// different words in the index for each time number.
+//
+// "pages" FTS table:
+// url URL of the page so searches will match the URL.
+// title Title of the page.
+// body Body of the page.
+//
+// "info" regular table:
+// time Time the corresponding FTS entry was visited.
+//
+// We do joins across these two tables by using their internal rowids, which we
+// keep in sync between the two tables. The internal rowid is the only part of
+// an FTS table that is indexed like a normal table, and the index over it is
+// free since sqlite always indexes the internal rowid.
+
+namespace history {
+
+namespace {
+
+const int kCurrentVersionNumber = 1;
+
+// Snippet computation relies on the index of the columns in the original
+// create statement. These are the 0-based indices (as strings) of the
+// corresponding columns.
+const char kTitleColumnIndex[] = "1";
+const char kBodyColumnIndex[] = "2";
+
+// The string prepended to the database identifier to generate the filename.
+const wchar_t kFilePrefix[] = L"History Index ";
+const size_t kFilePrefixLen = arraysize(kFilePrefix) - 1; // Don't count NULL.
+
+// We do not allow rollback, but this simple scoper makes it easy to always
+// remember to commit a begun transaction. This protects against some errors
+// caused by a crash in the middle of a transaction, although doesn't give us
+// the full protection of a transaction's rollback abilities.
+class ScopedTransactionCommitter {
+ public:
+ ScopedTransactionCommitter(TextDatabase* db) : db_(db) {
+ db_->BeginTransaction();
+ }
+ ~ScopedTransactionCommitter() {
+ db_->CommitTransaction();
+ }
+ private:
+ TextDatabase* db_;
+};
+
+
+} // namespace
+
+TextDatabase::TextDatabase(const std::wstring& path,
+ DBIdent id,
+ bool allow_create)
+ : db_(NULL),
+ statement_cache_(NULL),
+ path_(path),
+ ident_(id),
+ allow_create_(allow_create),
+ transaction_nesting_(0) {
+ // Compute the file name.
+ file_name_ = path_;
+ file_util::AppendToPath(&file_name_, IDToFileName(ident_));
+}
+
+TextDatabase::~TextDatabase() {
+ if (statement_cache_) {
+ // Must release these statements before closing the DB.
+ delete statement_cache_;
+ statement_cache_ = NULL;
+ }
+ if (db_) {
+ sqlite3_close(db_);
+ db_ = NULL;
+ }
+}
+
+// static
+const wchar_t* TextDatabase::file_base() {
+ return kFilePrefix;
+}
+
+// static
+std::wstring TextDatabase::IDToFileName(DBIdent id) {
+ // Identifiers are intended to be a combination of the year and month, for
+ // example, 200801 for January 2008. We convert this to
+ // "History Index 2008-01". However, we don't make assumptions about this
+ // scheme: the caller should assign IDs as it feels fit with the knowledge
+ // that they will apppear on disk in this form.
+ return StringPrintf(L"%s%d-%02d", file_base(), id / 100, id % 100);
+}
+
+// static
+TextDatabase::DBIdent TextDatabase::FileNameToID(const std::wstring& file_path){
+ std::wstring file_name = file_util::GetFilenameFromPath(file_path);
+
+ // We don't actually check the prefix here. Since the file system could
+ // be case insensitive in ways we can't predict (NTFS), checking could
+ // potentially be the wrong thing to do. Instead, we just look for a suffix.
+ static const int kIDStringLength = 7; // Room for "xxxx-xx".
+ if (file_name.length() < kIDStringLength)
+ return 0;
+ const wchar_t* number_begin =
+ &file_name[file_name.length() - kIDStringLength];
+
+ int year, month;
+ if (swscanf_s(number_begin, L"%d-%d", &year, &month) != 2)
+ return 0; // Unable to get both numbers.
+
+ return year * 100 + month;
+}
+
+bool TextDatabase::Init() {
+ // Make sure, if we're not allowed to create the file, that it exists.
+ if (!allow_create_) {
+ if (!file_util::PathExists(file_name_))
+ return false;
+ }
+
+ // Attach the database to our index file.
+ if (sqlite3_open(WideToUTF8(file_name_).c_str(), &db_) != SQLITE_OK)
+ return false;
+ statement_cache_ = new SqliteStatementCache(db_);
+
+ // Set the database page size to something a little larger to give us
+ // better performance (we're typically seek rather than bandwidth limited).
+ // This only has an effect before any tables have been created, otherwise
+ // this is a NOP. Must be a power of 2 and a max of 8192.
+ sqlite3_exec(db_, "PRAGMA page_size=4096", NULL, NULL, NULL);
+
+ // The default cache size is 2000 which give >8MB of data. Since we will often
+ // have 2-3 of these objects, each with their own 8MB, this adds up very fast.
+ // We therefore reduce the size so when there are multiple objects, we're not
+ // too big.
+ sqlite3_exec(db_, "PRAGMA cache_size=512", NULL, NULL, NULL);
+
+ // Run the database in exclusive mode. Nobody else should be accessing the
+ // database while we're running, and this will give somewhat improved perf.
+ sqlite3_exec(db_, "PRAGMA locking_mode=EXCLUSIVE", NULL, NULL, NULL);
+
+ // Meta table tracking version information.
+ if (!meta_table_.Init(std::string(), kCurrentVersionNumber, db_))
+ return false;
+ if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) {
+ // This version is too new. We don't bother notifying the user on this
+ // error, and just fail to use the file. Normally if they have version skew,
+ // they will get it for the main history file and it won't be necessary
+ // here. If that's not the case, since this is only indexed data, it's
+ // probably better to just not give FTS results than strange errors when
+ // everything else is working OK.
+ return false;
+ }
+
+ return CreateTables();
+}
+
+void TextDatabase::BeginTransaction() {
+ if (!transaction_nesting_)
+ sqlite3_exec(db_, "BEGIN TRANSACTION", NULL, NULL, NULL);
+ transaction_nesting_++;
+}
+
+void TextDatabase::CommitTransaction() {
+ DCHECK(transaction_nesting_);
+ transaction_nesting_--;
+ if (!transaction_nesting_)
+ sqlite3_exec(db_, "COMMIT", NULL, NULL, NULL);
+}
+
+bool TextDatabase::CreateTables() {
+ // FTS table of page contents.
+ if (!DoesSqliteTableExist(db_, "pages")) {
+ if (sqlite3_exec(db_,
+ "CREATE VIRTUAL TABLE pages USING fts2("
+ "TOKENIZE icu,"
+ "url LONGVARCHAR,"
+ "title LONGVARCHAR,"
+ "body LONGVARCHAR)", NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ }
+
+ // Non-FTS table containing URLs and times so we can efficiently find them
+ // using a regular index (all FTS columns are special and are treated as
+ // full-text-search, which is not what we want when retrieving this data).
+ if (!DoesSqliteTableExist(db_, "info")) {
+ // Note that there is no point in creating an index over time. Since
+ // we must always query the entire FTS table (it can not efficiently do
+ // subsets), we will always end up doing that first, and joining the info
+ // table off of that.
+ if (sqlite3_exec(db_, "CREATE TABLE info(time INTEGER NOT NULL)",
+ NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ }
+
+ // Create the index. This will fail when the index already exists, so we just
+ // ignore the error.
+ sqlite3_exec(db_, "CREATE INDEX info_time ON info(time)", NULL, NULL, NULL);
+ return true;
+}
+
+bool TextDatabase::AddPageData(Time time,
+ const std::string& url,
+ const std::string& title,
+ const std::string& contents) {
+ ScopedTransactionCommitter committer(this);
+
+ // Add to the pages table.
+ SQLITE_UNIQUE_STATEMENT(add_to_pages, *statement_cache_,
+ "INSERT INTO pages(url,title,body)VALUES(?,?,?)");
+ if (!add_to_pages.is_valid())
+ return false;
+ add_to_pages->bind_string(0, url);
+ add_to_pages->bind_string(1, title);
+ add_to_pages->bind_string(2, contents);
+ if (add_to_pages->step() != SQLITE_DONE) {
+ NOTREACHED() << sqlite3_errmsg(db_);
+ return false;
+ }
+
+ int64 rowid = sqlite3_last_insert_rowid(db_);
+
+ // Add to the info table with the same rowid.
+ SQLITE_UNIQUE_STATEMENT(add_to_info, *statement_cache_,
+ "INSERT INTO info(rowid,time) VALUES(?,?)");
+ if (!add_to_info.is_valid())
+ return false;
+ add_to_info->bind_int64(0, rowid);
+ add_to_info->bind_int64(1, time.ToInternalValue());
+ if (add_to_info->step() != SQLITE_DONE) {
+ NOTREACHED() << sqlite3_errmsg(db_);
+ return false;
+ }
+
+ return true;
+}
+
+void TextDatabase::DeletePageData(Time time, const std::string& url) {
+ // First get all rows that match. Selecing on time (which has an index) allows
+ // us to avoid brute-force searches on the full-text-index table (there will
+ // generally be only one match per time).
+ SQLITE_UNIQUE_STATEMENT(select_ids, *statement_cache_,
+ "SELECT info.rowid "
+ "FROM info JOIN pages ON info.rowid = pages.rowid "
+ "WHERE info.time=? AND pages.url=?");
+ if (!select_ids.is_valid())
+ return;
+ select_ids->bind_int64(0, time.ToInternalValue());
+ select_ids->bind_string(1, url);
+ std::set<int64> rows_to_delete;
+ while (select_ids->step() == SQLITE_ROW)
+ rows_to_delete.insert(select_ids->column_int64(0));
+
+ // Delete from the pages table.
+ SQLITE_UNIQUE_STATEMENT(delete_page, *statement_cache_,
+ "DELETE FROM pages WHERE rowid=?");
+ if (!delete_page.is_valid())
+ return;
+ for (std::set<int64>::const_iterator i = rows_to_delete.begin();
+ i != rows_to_delete.end(); ++i) {
+ delete_page->bind_int64(0, *i);
+ delete_page->step();
+ delete_page->reset();
+ }
+
+ // Delete from the info table.
+ SQLITE_UNIQUE_STATEMENT(delete_info, *statement_cache_,
+ "DELETE FROM info WHERE rowid=?");
+ if (!delete_info.is_valid())
+ return;
+ for (std::set<int64>::const_iterator i = rows_to_delete.begin();
+ i != rows_to_delete.end(); ++i) {
+ delete_info->bind_int64(0, *i);
+ delete_info->step();
+ delete_info->reset();
+ }
+}
+
+void TextDatabase::Optimize() {
+ SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
+ "SELECT OPTIMIZE(pages) FROM pages LIMIT 1");
+ if (!statement.is_valid())
+ return;
+ statement->step();
+}
+
+void TextDatabase::GetTextMatches(const std::string& query,
+ const QueryOptions& options,
+ std::vector<Match>* results,
+ URLSet* found_urls,
+ Time* first_time_searched) {
+ *first_time_searched = options.begin_time;
+
+ SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
+ "SELECT url, title, time, offsets(pages), body "
+ "FROM pages LEFT OUTER JOIN info ON pages.rowid = info.rowid "
+ "WHERE pages MATCH ? AND time >= ? AND time < ? "
+ "ORDER BY time DESC "
+ "LIMIT ?");
+ if (!statement.is_valid())
+ return;
+
+ // When their values indicate "unspecified", saturate the numbers to the max
+ // or min to get the correct result.
+ int64 effective_begin_time = options.begin_time.is_null() ?
+ 0 : options.begin_time.ToInternalValue();
+ int64 effective_end_time = options.end_time.is_null() ?
+ std::numeric_limits<int64>::max() : options.end_time.ToInternalValue();
+ int effective_max_count = options.max_count ?
+ options.max_count : std::numeric_limits<int>::max();
+
+ statement->bind_string(0, query);
+ statement->bind_int64(1, effective_begin_time);
+ statement->bind_int64(2, effective_end_time);
+ statement->bind_int(3, effective_max_count);
+
+ while (statement->step() == SQLITE_ROW) {
+ // TODO(brettw) allow canceling the query in the middle.
+ // if (canceled_or_something)
+ // break;
+
+ GURL url(statement->column_string(0));
+ if (options.most_recent_visit_only) {
+ URLSet::const_iterator found_url = found_urls->find(url);
+ if (found_url != found_urls->end())
+ continue; // Don't add this duplicate when unique URLs are requested.
+ }
+
+ // Fill the results into the vector (avoid copying the URL with Swap()).
+ results->resize(results->size() + 1);
+ Match& match = results->at(results->size() - 1);
+ match.url.Swap(&url);
+
+ match.title = statement->column_string16(1);
+ match.time = Time::FromInternalValue(statement->column_int64(2));
+
+ // Extract any matches in the title.
+ std::string offsets_str = statement->column_string(3);
+ Snippet::ExtractMatchPositions(offsets_str, kTitleColumnIndex,
+ &match.title_match_positions);
+ Snippet::ConvertMatchPositionsToWide(statement->column_string(1),
+ &match.title_match_positions);
+
+ // Extract the matches in the body.
+ Snippet::MatchPositions match_positions;
+ Snippet::ExtractMatchPositions(offsets_str, kBodyColumnIndex,
+ &match_positions);
+
+ // Compute the snippet based on those matches.
+ std::string body = statement->column_string(4);
+ match.snippet.ComputeSnippet(match_positions, body);
+ }
+
+ // When we have returned all the results possible (or determined that there
+ // are none), then we have searched all the time requested, so we can
+ // set the first_time_searched to that value.
+ if (results->size() == 0 ||
+ options.max_count == 0 || // Special case for wanting all the results.
+ static_cast<int>(results->size()) < options.max_count) {
+ *first_time_searched = options.begin_time;
+ } else {
+ // Since we got the results in order, we know the last item is the last
+ // time we considered.
+ *first_time_searched = results->back().time;
+ }
+
+ statement->reset();
+}
+
+} // namespace history
diff --git a/chrome/browser/history/text_database.h b/chrome/browser/history/text_database.h
new file mode 100644
index 0000000..f04f8c0
--- /dev/null
+++ b/chrome/browser/history/text_database.h
@@ -0,0 +1,197 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_TEXT_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_TEXT_DATABASE_H__
+
+#include <set>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/meta_table_helper.h"
+#include "chrome/common/sqlite_compiled_statement.h"
+#include "googleurl/src/gurl.h"
+
+struct sqlite3;
+
+namespace history {
+
+// Encapsulation of a full-text indexed database file.
+class TextDatabase {
+ public:
+ typedef int DBIdent;
+
+ typedef std::set<GURL> URLSet;
+
+ // Returned from the search function.
+ struct Match {
+ // URL of the match.
+ GURL url;
+
+ // The title is returned because the title in the text database and the URL
+ // database may differ. This happens because we capture the title when the
+ // body is captured, and don't update it later.
+ std::wstring title;
+
+ // Time the page that was returned was visited.
+ Time time;
+
+ // Identifies any found matches in the title of the document. These are not
+ // included in the snippet.
+ Snippet::MatchPositions title_match_positions;
+
+ // Snippet of the match we generated from the body.
+ Snippet snippet;
+ };
+
+ // Note: You must call init which must succeed before using this class.
+ //
+ // Computes the mathes for the query, returning results in decreasing order
+ // of visit time.
+ //
+ // This function will attach the new database to the given database
+ // connection. This allows one sqlite3 object to share many TextDatabases,
+ // meaning that they will all share the same cache, which allows us to limit
+ // the total size that text indexing databasii can take up.
+ //
+ // |file_name| is the name of the file on disk.
+ //
+ // ID is the identifier for the database. It should uniquely identify it among
+ // other databases on disk and in the sqlite connection.
+ //
+ // |allow_create| indicates if we want to allow creation of the file if it
+ // doesn't exist. For files associated with older time periods, we don't want
+ // to create them if they don't exist, so this flag would be false.
+ TextDatabase(const std::wstring& path,
+ DBIdent id,
+ bool allow_create);
+ ~TextDatabase();
+
+ // Initializes the database connection and creates the file if the class
+ // was created with |allow_create|. If the file couldn't be opened or
+ // created, this will return false. No other functions should be called
+ // after this.
+ bool Init();
+
+ // Allows updates to be batched. This gives higher performance when multiple
+ // updates are happening because every insert doesn't require a sync to disk.
+ // Transactions can be nested, only the outermost one will actually count.
+ void BeginTransaction();
+ void CommitTransaction();
+
+ // For testing, returns the file name of the database so it can be deleted
+ // after the test. This is valid even before Init() is called.
+ const std::wstring& file_name() const { return file_name_; }
+
+ // Returns a NULL-terminated string that is the base of history index files,
+ // which is the part before the database identifier. For example
+ // "History Index *". This is for finding existing database files.
+ static const wchar_t* file_base();
+
+ // Converts a filename on disk (optionally including a path) to a database
+ // identifier. If the filename doesn't have the correct format, returns 0.
+ static DBIdent FileNameToID(const std::wstring& file_path);
+
+ // Changing operations -------------------------------------------------------
+
+ // Adds the given data to the page. Returns true on success. The data should
+ // already be converted to UTF-8.
+ bool AddPageData(Time time,
+ const std::string& url,
+ const std::string& title,
+ const std::string& contents);
+
+ // Deletes the indexed data exactly matching the given URL/time pair.
+ void DeletePageData(Time time, const std::string& url);
+
+ // Optimizes the tree inside the database. This will, in addition to making
+ // access faster, remove any deleted data from the database (normally it is
+ // added again as "removed" and it is manually cleaned up when it decides to
+ // optimize it naturally). It is bad for privacy if a user is deleting a
+ // page from history but it still exists in the full text database in some
+ // form. This function will clean that up.
+ void Optimize();
+
+ // Querying ------------------------------------------------------------------
+
+ // Executes the given query. See QueryOptions for more info on input.
+ // Note that the options.only_starred is ignored since this database does not
+ // have access to star information.
+ //
+ // The results are appended to any existing ones in |*results|, and the first
+ // time considered for the output is in |first_time_searched|
+ // (see QueryResults for more).
+ //
+ // When |options.most_recent_visit_only|, any URLs found will be added to
+ // |unique_urls|. If a URL is already in the set, additional results will not
+ // be added (giving the ability to uniquify URL results, with the most recent
+ // If |most_recent_visit_only| is not set, |unique_urls| will be untouched.
+ //
+ // Callers must run QueryParser on the user text and pass the results of the
+ // QueryParser to this method as the query string.
+ void GetTextMatches(const std::string& query,
+ const QueryOptions& options,
+ std::vector<Match>* results,
+ URLSet* unique_urls,
+ Time* first_time_searched);
+
+ // Converts the given database identifier to a filename. This does not include
+ // the path, just the file and extension.
+ static std::wstring IDToFileName(DBIdent id);
+
+ private:
+ // Ensures that the tables and indices are created. Returns true on success.
+ bool CreateTables();
+
+ // See the constructor.
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+
+ const std::wstring path_;
+ const DBIdent ident_;
+ const bool allow_create_;
+
+ // Full file name of the file on disk, computed in Init().
+ std::wstring file_name_;
+
+ // Nesting levels of transactions. Since sqlite only allows one open
+ // transaction, we simulate nested transactions by mapping the outermost one
+ // to a real transaction. Since this object never needs to do ROLLBACK, losing
+ // the ability for all transactions to rollback is inconsequential.
+ int transaction_nesting_;
+
+ MetaTableHelper meta_table_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(TextDatabase);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_TEXT_DATABASE_H__
diff --git a/chrome/browser/history/text_database_manager.cc b/chrome/browser/history/text_database_manager.cc
new file mode 100644
index 0000000..e9588f6
--- /dev/null
+++ b/chrome/browser/history/text_database_manager.cc
@@ -0,0 +1,510 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/text_database_manager.h"
+
+#include "base/file_util.h"
+#include "base/histogram.h"
+#include "base/logging.h"
+#include "base/message_loop.h"
+#include "base/string_util.h"
+#include "chrome/common/mru_cache.h"
+
+namespace history {
+
+namespace {
+
+// The number of database files we will be attached to at once.
+const int kCacheDBSize = 5;
+
+std::string ConvertStringForIndexer(
+ const std::wstring& input) {
+ // TODO(evanm): other transformations here?
+ return WideToUTF8(CollapseWhitespace(input, false));
+}
+
+// Data older than this will be committed to the full text index even if we
+// haven't gotten a title and/or body.
+const int kExpirationSec = 20;
+
+} // namespace
+
+// TextDatabaseManager::PageInfo -----------------------------------------------
+
+TextDatabaseManager::PageInfo::PageInfo(URLID url_id,
+ VisitID visit_id,
+ Time visit_time)
+ : url_id_(url_id),
+ visit_id_(visit_id),
+ visit_time_(visit_time) {
+ added_time_ = TimeTicks::Now();
+}
+
+void TextDatabaseManager::PageInfo::set_title(const std::wstring& ttl) {
+ if (ttl.empty()) // Make the title nonempty when we set it for EverybodySet.
+ title_ = L" ";
+ else
+ title_ = ttl;
+}
+
+void TextDatabaseManager::PageInfo::set_body(const std::wstring& bdy) {
+ if (bdy.empty()) // Make the body nonempty when we set it for EverybodySet.
+ body_ = L" ";
+ else
+ body_ = bdy;
+}
+
+bool TextDatabaseManager::PageInfo::Expired(TimeTicks now) const {
+ return now - added_time_ > TimeDelta::FromSeconds(kExpirationSec);
+}
+
+// TextDatabaseManager ---------------------------------------------------------
+
+TextDatabaseManager::TextDatabaseManager(const std::wstring& dir,
+ VisitDatabase* visit_database)
+ : dir_(dir),
+ db_(NULL),
+ visit_database_(visit_database),
+ recent_changes_(RecentChangeList::NO_AUTO_EVICT),
+ transaction_nesting_(0),
+ db_cache_(DBCache::NO_AUTO_EVICT),
+ present_databases_loaded_(false),
+#pragma warning(suppress: 4355) // Okay to pass "this" here.
+ factory_(this) {
+}
+
+TextDatabaseManager::~TextDatabaseManager() {
+ if (transaction_nesting_)
+ CommitTransaction();
+}
+
+// static
+TextDatabase::DBIdent TextDatabaseManager::TimeToID(Time time) {
+ Time::Exploded exploded;
+ time.UTCExplode(&exploded);
+
+ // We combine the month and year into a 6-digit number (200801 for
+ // January, 2008). The month is 1-based.
+ return exploded.year * 100 + exploded.month;
+}
+
+// static
+Time TextDatabaseManager::IDToTime(TextDatabase::DBIdent id) {
+ Time::Exploded exploded;
+ memset(&exploded, 0, sizeof(Time::Exploded));
+ exploded.year = id / 100;
+ exploded.month = id % 100;
+ return Time::FromUTCExploded(exploded);
+}
+
+bool TextDatabaseManager::Init() {
+ // Start checking recent changes and committing them.
+ ScheduleFlushOldChanges();
+ return true;
+}
+
+void TextDatabaseManager::BeginTransaction() {
+ transaction_nesting_++;
+}
+
+void TextDatabaseManager::CommitTransaction() {
+ DCHECK(transaction_nesting_);
+ transaction_nesting_--;
+ if (transaction_nesting_)
+ return; // Still more nesting of transactions before committing.
+
+ // Commit all databases with open transactions on them.
+ for (DBIdentSet::const_iterator i = open_transactions_.begin();
+ i != open_transactions_.end(); ++i) {
+ DBCache::iterator iter = db_cache_.Get(*i);
+ if (iter == db_cache_.end()) {
+ NOTREACHED() << "All open transactions should be cached.";
+ continue;
+ }
+ iter->second->CommitTransaction();
+ }
+ open_transactions_.clear();
+
+ // Now that the transaction is over, we can expire old connections.
+ db_cache_.ShrinkToSize(kCacheDBSize);
+}
+
+void TextDatabaseManager::InitDBList() {
+ if (present_databases_loaded_)
+ return;
+
+ present_databases_loaded_ = true;
+
+ // Find files on disk matching our pattern so we can quickly test for them.
+ file_util::FileEnumerator enumerator(dir_, false,
+ file_util::FileEnumerator::FILES,
+ std::wstring(TextDatabase::file_base()) + L"*");
+ std::wstring cur_file;
+ while (!(cur_file = enumerator.Next()).empty()) {
+ // Convert to the number representing this file.
+ TextDatabase::DBIdent id = TextDatabase::FileNameToID(cur_file);
+ if (id) // Will be 0 on error.
+ present_databases_.insert(id);
+ }
+}
+
+void TextDatabaseManager::AddPageURL(const GURL& url,
+ URLID url_id,
+ VisitID visit_id,
+ Time time) {
+ // Delete any existing page info.
+ RecentChangeList::iterator found = recent_changes_.Peek(url);
+ if (found != recent_changes_.end())
+ recent_changes_.Erase(found);
+
+ // Just save this info for later. We will save it when it expires or when all
+ // the data is complete.
+ recent_changes_.Put(url, PageInfo(url_id, visit_id, time));
+}
+
+void TextDatabaseManager::AddPageTitle(const GURL& url,
+ const std::wstring& title) {
+ RecentChangeList::iterator found = recent_changes_.Peek(url);
+ if (found == recent_changes_.end())
+ return; // We don't know about this page, give up.
+
+ PageInfo& info = found->second;
+ if (info.has_body()) {
+ // This info is complete, write to the database.
+ AddPageData(url, info.url_id(), info.visit_id(), info.visit_time(),
+ title, info.body());
+ recent_changes_.Erase(found);
+ return;
+ }
+
+ info.set_title(title);
+}
+
+void TextDatabaseManager::AddPageContents(const GURL& url,
+ const std::wstring& body) {
+ RecentChangeList::iterator found = recent_changes_.Peek(url);
+ if (found == recent_changes_.end())
+ return; // We don't know about this page, give up.
+
+ PageInfo& info = found->second;
+ if (info.has_title()) {
+ // This info is complete, write to the database.
+ AddPageData(url, info.url_id(), info.visit_id(), info.visit_time(),
+ info.title(), body);
+ recent_changes_.Erase(found);
+ return;
+ }
+
+ info.set_body(body);
+}
+
+bool TextDatabaseManager::AddPageData(const GURL& url,
+ URLID url_id,
+ VisitID visit_id,
+ Time visit_time,
+ const std::wstring& title,
+ const std::wstring& body) {
+ TextDatabase* db = GetDBForTime(visit_time, true);
+ if (!db)
+ return false;
+
+ TimeTicks beginning_time = TimeTicks::Now();
+
+ // First delete any recently-indexed data for this page. This will delete
+ // anything in the main database, but we don't bother looking through the
+ // archived database.
+ VisitVector visits;
+ visit_database_->GetVisitsForURL(url_id, &visits);
+ size_t our_visit_row_index = visits.size();
+ for (size_t i = 0; i < visits.size(); i++) {
+ // While we're going trough all the visits, also find our row so we can
+ // avoid another DB query.
+ if (visits[i].visit_id == visit_id) {
+ our_visit_row_index = i;
+ } else if (visits[i].is_indexed) {
+ visits[i].is_indexed = false;
+ visit_database_->UpdateVisitRow(visits[i]);
+ DeletePageData(visits[i].visit_time, url, NULL);
+ }
+ }
+
+ if (visit_id) {
+ // We're supposed to update the visit database.
+ if (our_visit_row_index >= visits.size()) {
+ NOTREACHED() << "We should always have found a visit when given an ID.";
+ return false;
+ }
+
+ DCHECK(visit_time == visits[our_visit_row_index].visit_time);
+
+ // Update the visit database to reference our addition.
+ visits[our_visit_row_index].is_indexed = true;
+ if (!visit_database_->UpdateVisitRow(visits[our_visit_row_index]))
+ return false;
+ }
+
+ // Now index the data.
+ std::string url_str = URLDatabase::GURLToDatabaseURL(url);
+ bool success = db->AddPageData(visit_time, url_str,
+ ConvertStringForIndexer(title),
+ ConvertStringForIndexer(body));
+
+ HISTOGRAM_TIMES(L"History.AddFTSData",
+ TimeTicks::Now() - beginning_time);
+ return success;
+}
+
+void TextDatabaseManager::DeletePageData(Time time, const GURL& url,
+ ChangeSet* change_set) {
+ TextDatabase::DBIdent db_ident = TimeToID(time);
+
+ // We want to open the database for writing, but only if it exists. To
+ // achieve this, we check whether it exists by saying we're not going to
+ // write to it (avoiding the autocreation code normally called when writing)
+ // and then access it for writing only if it succeeds.
+ TextDatabase* db = GetDB(db_ident, false);
+ if (!db)
+ return;
+ db = GetDB(db_ident, true);
+
+ if (change_set)
+ change_set->Add(db_ident);
+
+ db->DeletePageData(time, URLDatabase::GURLToDatabaseURL(url));
+}
+
+void TextDatabaseManager::DeleteFromUncommitted(Time begin, Time end) {
+ // First find the beginning of the range to delete. Recall that the list
+ // has the most recent item at the beginning. There won't normally be very
+ // many items, so a brute-force search is fine.
+ RecentChangeList::iterator cur = recent_changes_.begin();
+ if (!end.is_null()) {
+ // Walk from the beginning of the list backwards in time to find the newest
+ // entry that should be deleted.
+ while (cur != recent_changes_.end() && cur->second.visit_time() >= end)
+ ++cur;
+ }
+
+ // Now delete all visits up to the oldest one we were supposed to delete.
+ // Note that if begin is_null, it will be less than or equal to any other
+ // time.
+ while (cur != recent_changes_.end() && cur->second.visit_time() >= begin)
+ cur = recent_changes_.Erase(cur);
+}
+
+void TextDatabaseManager::DeleteURLFromUncommitted(const GURL& url) {
+ RecentChangeList::iterator found = recent_changes_.Peek(url);
+ if (found == recent_changes_.end())
+ return; // We don't know about this page, give up.
+ recent_changes_.Erase(found);
+}
+
+void TextDatabaseManager::DeleteAll() {
+ DCHECK(transaction_nesting_ == 0) << "Calling deleteAll in a transaction.";
+
+ InitDBList();
+
+ // Close all open databases.
+ db_cache_.ShrinkToSize(0);
+
+ // Now go through and delete all the files.
+ for (DBIdentSet::iterator i = present_databases_.begin();
+ i != present_databases_.end(); ++i) {
+ std::wstring file_name(dir_);
+ file_util::AppendToPath(&file_name, TextDatabase::IDToFileName(*i));
+ file_util::Delete(file_name, false);
+ }
+}
+
+void TextDatabaseManager::OptimizeChangedDatabases(
+ const ChangeSet& change_set) {
+ for (ChangeSet::DBSet::const_iterator i =
+ change_set.changed_databases_.begin();
+ i != change_set.changed_databases_.end(); ++i) {
+ // We want to open the database for writing, but only if it exists. To
+ // achieve this, we check whether it exists by saying we're not going to
+ // write to it (avoiding the autocreation code normally called when writing)
+ // and then access it for writing only if it succeeds.
+ TextDatabase* db = GetDB(*i, false);
+ if (!db)
+ continue;
+ db = GetDB(*i, true);
+ if (!db)
+ continue; // The file may have changed or something.
+ db->Optimize();
+ }
+}
+
+void TextDatabaseManager::GetTextMatches(
+ const std::wstring& query,
+ const QueryOptions& options,
+ std::vector<TextDatabase::Match>* results,
+ Time* first_time_searched) {
+ results->clear();
+
+ InitDBList();
+ if (present_databases_.empty()) {
+ // Nothing to search.
+ *first_time_searched = options.begin_time;
+ return;
+ }
+
+ // Get the query into the proper format for the individual DBs.
+ std::wstring fts_query_wide;
+ query_parser_.ParseQuery(query, &fts_query_wide);
+ std::string fts_query = WideToUTF8(fts_query_wide);
+
+ // Need a copy of the options so we can modify the max count for each call
+ // to the individual databases.
+ QueryOptions cur_options(options);
+
+ // Compute the minimum and maximum values for the identifiers that could
+ // encompass the input time range.
+ TextDatabase::DBIdent min_ident = options.begin_time.is_null() ?
+ *present_databases_.begin() :
+ TimeToID(options.begin_time);
+ TextDatabase::DBIdent max_ident = options.end_time.is_null() ?
+ *present_databases_.rbegin() :
+ TimeToID(options.end_time);
+
+ // Iterate over the databases from the most recent backwards.
+ bool checked_one = false;
+ TextDatabase::URLSet found_urls;
+ for (DBIdentSet::reverse_iterator i = present_databases_.rbegin();
+ i != present_databases_.rend();
+ ++i) {
+ // TODO(brettw) allow canceling the query in the middle.
+ // if (canceled_or_something)
+ // break;
+
+ // This code is stupid, we just loop until we find the correct starting
+ // time range rather than search in an intelligent way. Users will have a
+ // few dozen files at most, so this should not be an issue.
+ if (*i > max_ident)
+ continue; // Haven't gotten to the time range yet.
+ if (*i < min_ident)
+ break; // Covered all the time range.
+
+ TextDatabase* cur_db = GetDB(*i, false);
+ if (!cur_db)
+ continue;
+
+ // Adjust the max count according to how many results we've already got.
+ if (options.max_count) {
+ cur_options.max_count = options.max_count -
+ static_cast<int>(results->size());
+ }
+
+ // Since we are going backwards in time, it is always OK to pass the
+ // current first_time_searched, since it will always be smaller than
+ // any previous set.
+ cur_db->GetTextMatches(fts_query, cur_options,
+ results, &found_urls, first_time_searched);
+ checked_one = true;
+
+ DCHECK(options.max_count == 0 ||
+ static_cast<int>(results->size()) <= options.max_count);
+ if (options.max_count &&
+ static_cast<int>(results->size()) >= options.max_count)
+ break; // Got the max number of results.
+ }
+
+ // When there were no databases in the range, we need to fix up the min time.
+ if (!checked_one)
+ *first_time_searched = options.begin_time;
+}
+
+TextDatabase* TextDatabaseManager::GetDB(TextDatabase::DBIdent id,
+ bool for_writing) {
+ DBCache::iterator found_db = db_cache_.Get(id);
+ if (found_db != db_cache_.end()) {
+ if (transaction_nesting_ && for_writing &&
+ open_transactions_.find(id) == open_transactions_.end()) {
+ // If we currently have an open transaction, that database is not yet
+ // part of the transaction, and the database will be written to, it needs
+ // to be part of our transaction.
+ found_db->second->BeginTransaction();
+ open_transactions_.insert(id);
+ }
+ return found_db->second;
+ }
+
+ // Need to make the database.
+ TextDatabase* new_db = new TextDatabase(dir_, id, for_writing);
+ if (!new_db->Init()) {
+ delete new_db;
+ return NULL;
+ }
+ db_cache_.Put(id, new_db);
+ present_databases_.insert(id);
+
+ if (transaction_nesting_ && for_writing) {
+ // If we currently have an open transaction and the new database will be
+ // written to, it needs to be part of our transaction.
+ new_db->BeginTransaction();
+ open_transactions_.insert(id);
+ }
+
+ // When no transaction is open, allow this new one to kick out an old one.
+ if (!transaction_nesting_)
+ db_cache_.ShrinkToSize(kCacheDBSize);
+
+ return new_db;
+}
+
+TextDatabase* TextDatabaseManager::GetDBForTime(Time time,
+ bool create_if_necessary) {
+ return GetDB(TimeToID(time), create_if_necessary);
+}
+
+void TextDatabaseManager::ScheduleFlushOldChanges() {
+ factory_.RevokeAll();
+ MessageLoop::current()->PostDelayedTask(FROM_HERE, factory_.NewRunnableMethod(
+ &TextDatabaseManager::FlushOldChanges),
+ kExpirationSec * Time::kMillisecondsPerSecond);
+}
+
+void TextDatabaseManager::FlushOldChanges() {
+ FlushOldChangesForTime(TimeTicks::Now());
+}
+
+void TextDatabaseManager::FlushOldChangesForTime(TimeTicks now) {
+ // The end of the list is the oldest, so we just start from there committing
+ // things until we get something too new.
+ RecentChangeList::reverse_iterator i = recent_changes_.rbegin();
+ while (i != recent_changes_.rend() && i->second.Expired(now)) {
+ AddPageData(i->first, i->second.url_id(), i->second.visit_id(),
+ i->second.visit_time(), i->second.title(), i->second.body());
+ i = recent_changes_.Erase(i);
+ }
+
+ ScheduleFlushOldChanges();
+}
+
+} // namespace history
diff --git a/chrome/browser/history/text_database_manager.h b/chrome/browser/history/text_database_manager.h
new file mode 100644
index 0000000..2506835
--- /dev/null
+++ b/chrome/browser/history/text_database_manager.h
@@ -0,0 +1,325 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_TEXT_DATABASE_MANAGER_H__
+#define CHROME_BROWSER_HISTORY_TEXT_DATABASE_MANAGER_H__
+
+#include <set>
+#include <vector>
+
+#include "base/basictypes.h"
+#include "base/task.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/history/text_database.h"
+#include "chrome/browser/history/query_parser.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/browser/history/visit_database.h"
+#include "chrome/common/mru_cache.h"
+#include "testing/gtest/include/gtest/gtest_prod.h"
+
+struct sqlite3;
+
+namespace history {
+
+// Manages a set of text databases representing different time periods. This
+// will page them in and out as necessary, and will manage queries for times
+// spanning multiple databases.
+//
+// It will also keep a list of partial changes, such as page adds and title and
+// body sets, all of which come in at different times for a given page. When
+// all data is received or enough time has elapsed since adding, the indexed
+// data will be comitted.
+//
+// This allows us to minimize inserts and modifications, which are slow for the
+// full text database, since each page's information is added exactly once.
+//
+// Note: be careful to delete the relevant entries from this uncommitted list
+// when clearing history or this information may get added to the database soon
+// after the clear.
+class TextDatabaseManager {
+ public:
+ // Tracks a set of changes (only deletes need to be supported now) to the
+ // databases. This is opaque to the caller, but allows it to pass back a list
+ // of all database that it has caused a change to.
+ //
+ // This is necessary for the feature where we optimize full text databases
+ // which have changed as a result of the user deleting history via
+ // OptimizeChangedDatabases. We want to do each affected database only once at
+ // the end of the delete, but we don't want the caller to have to worry about
+ // our internals.
+ class ChangeSet {
+ public:
+ ChangeSet() {}
+
+ private:
+ friend class TextDatabaseManager;
+
+ typedef std::set<TextDatabase::DBIdent> DBSet;
+
+ void Add(TextDatabase::DBIdent id) { changed_databases_.insert(id); }
+
+ DBSet changed_databases_;
+ };
+
+ // You must call Init() to complete initialization.
+ //
+ // |dir| is the directory that will hold the full text database files (there
+ // will be many files named by their date ranges).
+ //
+ // The visit database is a pointer owned by the caller for the main database
+ // (of recent visits). The visit database will be updated to refer to the
+ // added text database entries.
+ explicit TextDatabaseManager(const std::wstring& dir,
+ VisitDatabase* visit_database);
+ ~TextDatabaseManager();
+
+ // Must call before using other functions. If it returns false, no other
+ // functions should be called.
+ bool Init();
+
+ // Allows scoping updates. This also allows things to go faster since every
+ // page add doesn't need to be committed to disk (slow). Note that files will
+ // still get created during a transaction.
+ void BeginTransaction();
+ void CommitTransaction();
+
+ // Sets specific information for the given page to be added to the database.
+ // In normal operation, URLs will be added as the user visits them, the titles
+ // and bodies will come in some time after that. These changes will be
+ // automatically coalesced and added to the database some time in the future
+ // using AddPageData().
+ //
+ // AddPageURL must be called for a given URL (+ its corresponding ID) before
+ // either the title or body set. The visit ID specifies the visit that will
+ // get updated to refer to the full text indexed information. The visit time
+ // should be the time corresponding to that visit in the database.
+ void AddPageURL(const GURL& url, URLID url_id, VisitID visit_id,
+ Time visit_time);
+ void AddPageTitle(const GURL& url, const std::wstring& title);
+ void AddPageContents(const GURL& url, const std::wstring& body);
+
+ // Adds the given data to the appropriate database file, returning true on
+ // success. The visit database row identified by |visit_id| will be updated
+ // to refer to the full text index entry. If the visit ID is 0, the visit
+ // database will not be updated.
+ bool AddPageData(const GURL& url,
+ URLID url_id,
+ VisitID visit_id,
+ Time visit_time,
+ const std::wstring& title,
+ const std::wstring& body);
+
+ // Deletes the instance of indexed data identified by the given time and URL.
+ // Any changes will be tracked in the optional change set for use when calling
+ // OptimizeChangedDatabases later. change_set can be NULL.
+ void DeletePageData(Time time, const GURL& url,
+ ChangeSet* change_set);
+
+ // The text database manager keeps a list of changes that are made to the
+ // file AddPageURL/Title/Body that may not be committed to the database yet.
+ // This function removes entires from this list happening between the given
+ // time range. It is called when the user clears their history for a time
+ // range, and we don't want any of our data to "leak."
+ //
+ // Either or both times my be is_null to be unbounded in that direction. When
+ // non-null, the range is [begin, end).
+ void DeleteFromUncommitted(Time begin, Time end);
+
+ // Same as DeleteFromUncommitted but for a single URL.
+ void DeleteURLFromUncommitted(const GURL& url);
+
+ // Deletes all full text search data by removing the files from the disk.
+ // This must be called OUTSIDE of a transaction since it actually deletes the
+ // files rather than messing with the database.
+ void DeleteAll();
+
+ // Calls optimize on all the databases identified in a given change set (see
+ // the definition of ChangeSet above for more). Optimizing means that old data
+ // will be removed rather than marked unused.
+ void OptimizeChangedDatabases(const ChangeSet& change_set);
+
+ // Executes the given query. See QueryOptions for more info on input.
+ // Note that the options.only_starred is ignored since this database does not
+ // have access to star information.
+ //
+ // The results are filled into |results|, and the first time considered for
+ // the output is in |first_time_searched| (see QueryResults for more).
+ //
+ // This function will return more than one match per URL if there is more than
+ // one entry for that URL in the database.
+ void GetTextMatches(const std::wstring& query,
+ const QueryOptions& options,
+ std::vector<TextDatabase::Match>* results,
+ Time* first_time_searched);
+
+ private:
+ // These tests call ExpireRecentChangesForTime to force expiration.
+ FRIEND_TEST(TextDatabaseManagerTest, InsertPartial);
+ FRIEND_TEST(ExpireHistoryTest, DeleteURLAndFavicon);
+ FRIEND_TEST(ExpireHistoryTest, FlushRecentURLsUnstarred);
+
+ // Stores "recent stuff" that has happened with the page, since the page
+ // visit, title, and body all come in at different times.
+ class PageInfo {
+ public:
+ PageInfo(URLID url_id, VisitID visit_id, Time visit_time);
+
+ // Getters.
+ URLID url_id() const { return url_id_; }
+ VisitID visit_id() const { return visit_id_; }
+ Time visit_time() const { return visit_time_; }
+ const std::wstring& title() const { return title_; }
+ const std::wstring& body() const { return body_; }
+
+ // Setters, we can only update the title and body.
+ void set_title(const std::wstring& ttl);
+ void set_body(const std::wstring& bdy);
+
+ // Returns true if both the title or body of the entry has been set. Since
+ // both the title and body setters will "fix" empty strings to be a space,
+ // these indicate if the setter was ever called.
+ bool has_title() const { return !title_.empty(); }
+ bool has_body() { return !body_.empty(); }
+
+ // Returns true if this entry was added too long ago and we should give up
+ // waiting for more data. The current time is passed in as an argument so we
+ // can check many without re-querying the timer.
+ bool Expired(TimeTicks now) const;
+
+ private:
+ URLID url_id_;
+ VisitID visit_id_;
+
+ // Time of the visit of the URL. This will be the value stored in the URL
+ // and visit tables for the entry.
+ Time visit_time_;
+
+ // When this page entry was created. We have a cap on the maximum time that
+ // an entry will be in the queue before being flushed to the database.
+ TimeTicks added_time_;
+
+ // Will be the string " " when they are set to distinguish set and unset.
+ std::wstring title_;
+ std::wstring body_;
+ };
+
+ // Converts the given time to a database identifier or vice-versa.
+ static TextDatabase::DBIdent TimeToID(Time time);
+ static Time IDToTime(TextDatabase::DBIdent id);
+
+ // Returns a text database for the given identifier or time. This file will
+ // be created if it doesn't exist and |for_writing| is set. On error,
+ // including the case where the file doesn't exist and |for_writing|
+ // is false, it will return NULL.
+ //
+ // When |for_writing| is set, a transaction on the database will be opened
+ // if there is a transaction open on this manager.
+ //
+ // The pointer will be tracked in the cache. The caller should not store it
+ // or delete it since it will get automatically deleted as necessary.
+ TextDatabase* GetDB(TextDatabase::DBIdent id, bool for_writing);
+ TextDatabase* GetDBForTime(Time time, bool for_writing);
+
+ // Populates the present_databases_ list based on which files are on disk.
+ // When the list is already initialized, this will do nothing, so you can
+ // call it whenever you want to ensure the present_databases_ set is filled.
+ void InitDBList();
+
+ // Schedules a call to ExpireRecentChanges in the future.
+ void ScheduleFlushOldChanges();
+
+ // Checks the recent_changes_ list and commits partial data that has been
+ // around too long.
+ void FlushOldChanges();
+
+ // Given "now," this will expire old things from the recent_changes_ list.
+ // This is used as the backend for FlushOldChanges and is called directly
+ // by the unit tests with fake times.
+ void FlushOldChangesForTime(TimeTicks now);
+
+ // Directory holding our index files.
+ const std::wstring dir_;
+
+ // The database connection.
+ sqlite3* db_;
+ DBCloseScoper db_close_scoper_;
+
+ // Non-owning pointers to the recent history databases for URLs and visits.
+ VisitDatabase* visit_database_;
+
+ // Lists recent additions that we have not yet filled out with the title and
+ // body. Sorted by time, we will flush them when they are complete or have
+ // been in the queue too long without modification.
+ //
+ // We kind of abuse the MRUCache because we never move things around in it
+ // using Get. Instead, we keep them in the order they were inserted, since
+ // this is the metric we use to measure age. The MRUCache gives us an ordered
+ // list with fast lookup by URL.
+ typedef MRUCache<GURL, PageInfo> RecentChangeList;
+ RecentChangeList recent_changes_;
+
+ // Nesting levels of transactions. Since sqlite only allows one open
+ // transaction, we simulate nested transactions by mapping the outermost one
+ // to a real transaction. Since this object never needs to do ROLLBACK, losing
+ // the ability for all transactions to rollback is inconsequential.
+ int transaction_nesting_;
+
+ // The cache owns the TextDatabase pointers, they will be automagically
+ // deleted when the cache entry is removed or expired.
+ typedef OwningMRUCache<TextDatabase::DBIdent, TextDatabase*> DBCache;
+ DBCache db_cache_;
+
+ // Tells us about the existance of database files on disk. All existing
+ // databases will be in here, and non-existant ones will not, so we don't
+ // have to check the disk every time.
+ //
+ // This set is populated LAZILY by InitDBList(), you should call that function
+ // before accessing the list.
+ //
+ // Note that iterators will work on the keys in-order. Normally, reverse
+ // iterators will be used to iterate the keys in reverse-order.
+ typedef std::set<TextDatabase::DBIdent> DBIdentSet;
+ DBIdentSet present_databases_;
+ bool present_databases_loaded_; // Set by InitDBList when populated.
+
+ // Lists all databases with open transactions. These will have to be closed
+ // when the transaction is committed.
+ DBIdentSet open_transactions_;
+
+ QueryParser query_parser_;
+
+ // Generates tasks for our periodic checking of expired "recent changes".
+ ScopedRunnableMethodFactory<TextDatabaseManager> factory_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(TextDatabaseManager);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_TEXT_DATABASE_MANAGER_H__
diff --git a/chrome/browser/history/text_database_manager_unittest.cc b/chrome/browser/history/text_database_manager_unittest.cc
new file mode 100644
index 0000000..80d905d
--- /dev/null
+++ b/chrome/browser/history/text_database_manager_unittest.cc
@@ -0,0 +1,485 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/file_util.h"
+#include "chrome/browser/history/text_database_manager.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace history {
+
+namespace {
+
+const char* kURL1 = "http://www.google.com/asdf";
+const wchar_t* kTitle1 = L"Google A";
+const wchar_t* kBody1 = L"FOO page one.";
+
+const char* kURL2 = "http://www.google.com/qwer";
+const wchar_t* kTitle2 = L"Google B";
+const wchar_t* kBody2 = L"FOO two.";
+
+const char* kURL3 = "http://www.google.com/zxcv";
+const wchar_t* kTitle3 = L"Google C";
+const wchar_t* kBody3 = L"FOO drei";
+
+const char* kURL4 = "http://www.google.com/hjkl";
+const wchar_t* kTitle4 = L"Google D";
+const wchar_t* kBody4 = L"FOO lalala four.";
+
+const char* kURL5 = "http://www.google.com/uiop";
+const wchar_t* kTitle5 = L"Google cinq";
+const wchar_t* kBody5 = L"FOO page one.";
+
+class TextDatabaseManagerTest : public testing::Test {
+ public:
+ // Called manually by the test so it can report failure to initialize.
+ bool Init() {
+ return file_util::CreateNewTempDirectory(L"TestSearchTest", &dir_);
+ }
+
+ protected:
+ void SetUp() {
+ }
+
+ void TearDown() {
+ file_util::Delete(dir_, true);
+ }
+
+ // Directory containing the databases.
+ std::wstring dir_;
+};
+
+// This provides a simple implementation of a VisitDatabase using an in-memory
+// sqlite connection. The text database manager expects to be able to update
+// the visit database to keep in sync.
+class InMemVisitDB : public VisitDatabase {
+ public:
+ InMemVisitDB() {
+ sqlite3_open(":memory:", &db_);
+ statement_cache_ = new SqliteStatementCache(db_);
+ InitVisitTable();
+ }
+ ~InMemVisitDB() {
+ delete statement_cache_;
+ sqlite3_close(db_);
+ }
+
+ private:
+ virtual sqlite3* GetDB() { return db_; }
+ virtual SqliteStatementCache& GetStatementCache() {
+ return *statement_cache_;
+ }
+
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(InMemVisitDB);
+};
+
+// Adds all the pages once, and the first page once more in the next month.
+// The times of all the pages will be filled into |*times|.
+void AddAllPages(TextDatabaseManager& manager, VisitDatabase* visit_db,
+ std::vector<Time>* times) {
+ Time::Exploded exploded;
+ memset(&exploded, 0, sizeof(Time::Exploded));
+
+ // Put the visits in two different months so it will query across databases.
+ exploded.year = 2008;
+ exploded.month = 1;
+ exploded.day_of_month = 3;
+
+ VisitRow visit_row;
+ visit_row.url_id = 1;
+ visit_row.visit_time = Time::FromUTCExploded(exploded);
+ visit_row.referring_visit = 0;
+ visit_row.transition = 0;
+ visit_row.segment_id = 0;
+ visit_row.is_indexed = false;
+ VisitID visit_id = visit_db->AddVisit(&visit_row);
+
+ times->push_back(visit_row.visit_time);
+ manager.AddPageData(GURL(kURL1), visit_row.url_id, visit_row.visit_id,
+ visit_row.visit_time, kTitle1, kBody1);
+
+ exploded.day_of_month++;
+ visit_row.url_id = 2;
+ visit_row.visit_time = Time::FromUTCExploded(exploded);
+ visit_id = visit_db->AddVisit(&visit_row);
+ times->push_back(visit_row.visit_time);
+ manager.AddPageData(GURL(kURL2), visit_row.url_id, visit_row.visit_id,
+ visit_row.visit_time, kTitle2, kBody2);
+
+ exploded.day_of_month++;
+ visit_row.url_id = 2;
+ visit_row.visit_time = Time::FromUTCExploded(exploded);
+ visit_id = visit_db->AddVisit(&visit_row);
+ times->push_back(visit_row.visit_time);
+ manager.AddPageData(GURL(kURL3), visit_row.url_id, visit_row.visit_id,
+ visit_row.visit_time, kTitle3, kBody3);
+
+ // Put the next ones in the next month.
+ exploded.month++;
+ visit_row.url_id = 2;
+ visit_row.visit_time = Time::FromUTCExploded(exploded);
+ visit_id = visit_db->AddVisit(&visit_row);
+ times->push_back(visit_row.visit_time);
+ manager.AddPageData(GURL(kURL4), visit_row.url_id, visit_row.visit_id,
+ visit_row.visit_time, kTitle4, kBody4);
+
+ exploded.day_of_month++;
+ visit_row.url_id = 2;
+ visit_row.visit_time = Time::FromUTCExploded(exploded);
+ visit_id = visit_db->AddVisit(&visit_row);
+ times->push_back(visit_row.visit_time);
+ manager.AddPageData(GURL(kURL5), visit_row.url_id, visit_row.visit_id,
+ visit_row.visit_time, kTitle5, kBody5);
+
+ // Put the first one in again in the second month.
+ exploded.day_of_month++;
+ visit_row.url_id = 2;
+ visit_row.visit_time = Time::FromUTCExploded(exploded);
+ visit_id = visit_db->AddVisit(&visit_row);
+ times->push_back(visit_row.visit_time);
+ manager.AddPageData(GURL(kURL1), visit_row.url_id, visit_row.visit_id,
+ visit_row.visit_time, kTitle1, kBody1);
+}
+
+bool ResultsHaveURL(const std::vector<TextDatabase::Match>& results,
+ const char* url) {
+ GURL gurl(url);
+ for (size_t i = 0; i < results.size(); i++) {
+ if (results[i].url == gurl)
+ return true;
+ }
+ return false;
+}
+
+} // namespace
+
+// Tests basic querying.
+TEST_F(TextDatabaseManagerTest, InsertQuery) {
+ ASSERT_TRUE(Init());
+ InMemVisitDB visit_db;
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ std::vector<Time> times;
+ AddAllPages(manager, &visit_db, &times);
+
+ QueryOptions options;
+ options.begin_time = times[0] - TimeDelta::FromDays(100);
+ options.end_time = times[times.size() - 1] + TimeDelta::FromDays(100);
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+
+ // We should have matched every page.
+ EXPECT_EQ(6, results.size());
+ EXPECT_TRUE(ResultsHaveURL(results, kURL1));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL2));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL3));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL4));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL5));
+
+ // The first time searched should have been the first page's time or before
+ // (it could have eliminated some time for us).
+ EXPECT_TRUE(first_time_searched <= times[0]);
+}
+
+// Tests that adding page components piecemeal will get them added properly.
+// This does not supply a visit to update, this mode is used only by the unit
+// tests right now, but we test it anyway.
+TEST_F(TextDatabaseManagerTest, InsertCompleteNoVisit) {
+ ASSERT_TRUE(Init());
+ InMemVisitDB visit_db;
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ // First add one without a visit.
+ const GURL url(kURL1);
+ manager.AddPageURL(url, 0, 0, Time::Now());
+ manager.AddPageTitle(url, kTitle1);
+ manager.AddPageContents(url, kBody1);
+
+ // Check that the page got added.
+ QueryOptions options;
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ ASSERT_EQ(1, results.size());
+ EXPECT_EQ(kTitle1, results[0].title);
+}
+
+// Like InsertCompleteNoVisit but specifies a visit to update. We check that the
+// visit was updated properly.
+TEST_F(TextDatabaseManagerTest, InsertCompleteVisit) {
+ ASSERT_TRUE(Init());
+ InMemVisitDB visit_db;
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ // First add a visit to a page. We can just make up a URL ID since there is
+ // not actually any URL database around.
+ VisitRow visit;
+ visit.url_id = 1;
+ visit.visit_time = Time::Now();
+ visit.referring_visit = 0;
+ visit.transition = PageTransition::LINK;
+ visit.segment_id = 0;
+ visit.is_indexed = false;
+ visit_db.AddVisit(&visit);
+
+ // Add a full text indexed entry for that visit.
+ const GURL url(kURL2);
+ manager.AddPageURL(url, visit.url_id, visit.visit_id, visit.visit_time);
+ manager.AddPageContents(url, kBody2);
+ manager.AddPageTitle(url, kTitle2);
+
+ // Check that the page got added.
+ QueryOptions options;
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ ASSERT_EQ(1, results.size());
+ EXPECT_EQ(kTitle2, results[0].title);
+
+ // Check that the visit got updated for its new indexed state.
+ VisitRow out_visit;
+ ASSERT_TRUE(visit_db.GetRowForVisit(visit.visit_id, &out_visit));
+ EXPECT_TRUE(out_visit.is_indexed);
+}
+
+// Tests that partial inserts that expire are added to the database.
+TEST_F(TextDatabaseManagerTest, InsertPartial) {
+ ASSERT_TRUE(Init());
+ InMemVisitDB visit_db;
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ // Add the first one with just a URL.
+ GURL url1(kURL1);
+ manager.AddPageURL(url1, 0, 0, Time::Now());
+
+ // Now add a second one with a URL and title.
+ GURL url2(kURL2);
+ manager.AddPageURL(url2, 0, 0, Time::Now());
+ manager.AddPageTitle(url2, kTitle2);
+
+ // The third one has a URL and body.
+ GURL url3(kURL3);
+ manager.AddPageURL(url3, 0, 0, Time::Now());
+ manager.AddPageContents(url3, kBody3);
+
+ // Expire stuff very fast. This assumes that the time between the first
+ // AddPageURL and this line is less than the expiration time (20 seconds).
+ TimeTicks added_time = TimeTicks::Now();
+ TimeTicks expire_time = added_time + TimeDelta::FromSeconds(5);
+ manager.FlushOldChangesForTime(expire_time);
+
+ // Do a query, nothing should be added yet.
+ QueryOptions options;
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+ manager.GetTextMatches(L"google", options, &results, &first_time_searched);
+ ASSERT_EQ(0, results.size());
+
+ // Compute a time threshold that will cause everything to be flushed, and
+ // poke at the manager's internals to cause this to happen.
+ expire_time = added_time + TimeDelta::FromDays(1);
+ manager.FlushOldChangesForTime(expire_time);
+
+ // Now we should have all 3 URLs added.
+ manager.GetTextMatches(L"google", options, &results, &first_time_searched);
+ ASSERT_EQ(3, results.size());
+ EXPECT_TRUE(ResultsHaveURL(results, kURL1));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL2));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL3));
+}
+
+// Tests that changes get properly committed to disk.
+TEST_F(TextDatabaseManagerTest, Writing) {
+ ASSERT_TRUE(Init());
+
+ QueryOptions options;
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+
+ InMemVisitDB visit_db;
+
+ // Create the manager and write some stuff to it.
+ {
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ std::vector<Time> times;
+ AddAllPages(manager, &visit_db, &times);
+
+ // We should have matched every page.
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ EXPECT_EQ(6, results.size());
+ }
+ results.clear();
+
+ // Recreate the manager and make sure it finds the written stuff.
+ {
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ // We should have matched every page again.
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ EXPECT_EQ(6, results.size());
+ }
+}
+
+// Tests that changes get properly committed to disk, as in the Writing test
+// above, but when there is a transaction around the adds.
+TEST_F(TextDatabaseManagerTest, WritingTransaction) {
+ ASSERT_TRUE(Init());
+
+ QueryOptions options;
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+
+ InMemVisitDB visit_db;
+
+ // Create the manager and write some stuff to it.
+ {
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ std::vector<Time> times;
+ manager.BeginTransaction();
+ AddAllPages(manager, &visit_db, &times);
+ // "Forget" to commit, it should be autocommittedd for us.
+
+ // We should have matched every page.
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ EXPECT_EQ(6, results.size());
+ }
+ results.clear();
+
+ // Recreate the manager and make sure it finds the written stuff.
+ {
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ // We should have matched every page again.
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ EXPECT_EQ(6, results.size());
+ }
+}
+
+// Tests querying where the maximum number of items is met.
+TEST_F(TextDatabaseManagerTest, QueryMax) {
+ ASSERT_TRUE(Init());
+ InMemVisitDB visit_db;
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ std::vector<Time> times;
+ AddAllPages(manager, &visit_db, &times);
+
+ QueryOptions options;
+ options.begin_time = times[0] - TimeDelta::FromDays(100);
+ options.end_time = times[times.size() - 1] + TimeDelta::FromDays(100);
+ options.max_count = 2;
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+
+ // We should have gotten the last two pages as results (the first page is
+ // also the last).
+ EXPECT_EQ(2, results.size());
+ EXPECT_TRUE(first_time_searched <= times[4]);
+ EXPECT_TRUE(ResultsHaveURL(results, kURL5));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL1));
+
+ // Asking for 4 pages, the first one should be in another DB.
+ options.max_count = 4;
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+
+ EXPECT_EQ(4, results.size());
+ EXPECT_TRUE(first_time_searched <= times[4]);
+ EXPECT_TRUE(ResultsHaveURL(results, kURL3));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL4));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL5));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL1));
+}
+
+// Tests querying backwards in time in chunks.
+TEST_F(TextDatabaseManagerTest, QueryBackwards) {
+ ASSERT_TRUE(Init());
+ InMemVisitDB visit_db;
+ TextDatabaseManager manager(dir_, &visit_db);
+ ASSERT_TRUE(manager.Init());
+
+ std::vector<Time> times;
+ AddAllPages(manager, &visit_db, &times);
+
+ // First do a query for all time, but with a max of 2. This will give us the
+ // last two results and will tell us where to start searching when we want
+ // to go back in time.
+ QueryOptions options;
+ options.begin_time = times[0] - TimeDelta::FromDays(100);
+ options.end_time = times[times.size() - 1] + TimeDelta::FromDays(100);
+ options.max_count = 2;
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+
+ // Check that we got the last two results.
+ EXPECT_EQ(2, results.size());
+ EXPECT_TRUE(first_time_searched <= times[4]);
+ EXPECT_TRUE(ResultsHaveURL(results, kURL5));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL1));
+
+ // Query the previous two URLs and make sure we got the correct ones.
+ options.end_time = first_time_searched;
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ EXPECT_EQ(2, results.size());
+ EXPECT_TRUE(first_time_searched <= times[2]);
+ EXPECT_TRUE(ResultsHaveURL(results, kURL3));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL4));
+
+ // Query the previous two URLs...
+ options.end_time = first_time_searched;
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ EXPECT_EQ(2, results.size());
+ EXPECT_TRUE(first_time_searched <= times[0]);
+ EXPECT_TRUE(ResultsHaveURL(results, kURL2));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL1));
+
+ // Try to query some more, there should be no results.
+ options.end_time = first_time_searched;
+ manager.GetTextMatches(L"FOO", options, &results, &first_time_searched);
+ EXPECT_EQ(0, results.size());
+}
+
+} // namespace history \ No newline at end of file
diff --git a/chrome/browser/history/text_database_unittest.cc b/chrome/browser/history/text_database_unittest.cc
new file mode 100644
index 0000000..d3eb4dc
--- /dev/null
+++ b/chrome/browser/history/text_database_unittest.cc
@@ -0,0 +1,346 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/scoped_ptr.h"
+#include "base/string_util.h"
+#include "chrome/browser/history/text_database.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace history {
+
+namespace {
+
+// Note that all pages have "COUNTTAG" which allows us to count the number of
+// pages in the database withoujt adding any extra functions to the DB object.
+const char kURL1[] = "http://www.google.com/";
+const int kTime1 = 1000;
+const char kTitle1[] = "Google";
+const char kBody1[] = "COUNTTAG Web Images Maps News Shopping Gmail more My Account | "
+ "Sign out Advanced Search Preferences Language Tools Advertising Programs "
+ "- Business Solutions - About Google, 2008 Google";
+
+const char kURL2[] = "http://images.google.com/";
+const int kTime2 = 2000;
+const char kTitle2[] = "Google Image Search";
+const char kBody2[] = "COUNTTAG Web Images Maps News Shopping Gmail more My Account | "
+ "Sign out Advanced Image Search Preferences The most comprehensive image "
+ "search on the web. Want to help improve Google Image Search? Try Google "
+ "Image Labeler. Advertising Programs - Business Solutions - About Google "
+ "2008 Google";
+
+const char kURL3[] = "http://slashdot.org/";
+const int kTime3 = 3000;
+const char kTitle3[] = "Slashdot: News for nerds, stuff that matters";
+const char kBody3[] = "COUNTTAG Slashdot Log In Create Account Subscribe Firehose Why "
+ "Log In? Why Subscribe? Nickname Password Public Terminal Sections "
+ "Main Apple AskSlashdot Backslash Books Developers Games Hardware "
+ "Interviews IT Linux Mobile Politics Science YRO";
+
+// Returns the number of rows currently in the database.
+int RowCount(TextDatabase* db) {
+ QueryOptions options;
+ options.begin_time = Time::FromInternalValue(0);
+ // Leave end_time at now.
+
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+ TextDatabase::URLSet unique_urls;
+ db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
+ &first_time_searched);
+ return static_cast<int>(results.size());
+}
+
+// Adds each of the test pages to the database.
+void AddAllTestData(TextDatabase* db) {
+ EXPECT_TRUE(db->AddPageData(
+ Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1));
+ EXPECT_TRUE(db->AddPageData(
+ Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2));
+ EXPECT_TRUE(db->AddPageData(
+ Time::FromInternalValue(kTime3), kURL3, kTitle3, kBody3));
+ EXPECT_EQ(3, RowCount(db));
+}
+
+bool ResultsHaveURL(const std::vector<TextDatabase::Match>& results,
+ const char* url) {
+ GURL gurl(url);
+ for (size_t i = 0; i < results.size(); i++) {
+ if (results[i].url == gurl)
+ return true;
+ }
+ return false;
+}
+
+} // namespace
+
+class TextDatabaseTest : public testing::Test {
+ public:
+ TextDatabaseTest() : db_(NULL) {
+ }
+
+ protected:
+ void SetUp() {
+ PathService::Get(base::DIR_TEMP, &temp_path_);
+ }
+
+ void TearDown() {
+ for (size_t i = 0; i < opened_files_.size(); i++)
+ file_util::Delete(opened_files_[i], false);
+ file_util::Delete(file_name_, false);
+ }
+
+ // Create databases with this function, which will ensure that the files are
+ // deleted on shutdown. Only open one database for each file. Returns NULL on
+ // failure.
+ //
+ // Set |delete_file| to delete any existing file. If we are trying to create
+ // the file for the first time, we don't want a previous test left in a
+ // weird state to have left a file that would affect us.
+ TextDatabase* CreateDB(TextDatabase::DBIdent id,
+ bool allow_create,
+ bool delete_file) {
+ TextDatabase* db = new TextDatabase(temp_path_, id, allow_create);
+
+ if (delete_file)
+ file_util::Delete(db->file_name(), false);
+
+ if (!db->Init()) {
+ delete db;
+ return NULL;
+ }
+ opened_files_.push_back(db->file_name());
+ return db;
+ }
+
+ // Directory containing the databases.
+ std::wstring temp_path_;
+
+ // Name of the main database file.
+ std::wstring file_name_;
+ sqlite3* db_;
+
+ std::vector<std::wstring> opened_files_;
+};
+
+TEST_F(TextDatabaseTest, AttachDetach) {
+ // First database with one page.
+ const int kIdee1 = 200801;
+ scoped_ptr<TextDatabase> db1(CreateDB(kIdee1, true, true));
+ ASSERT_TRUE(!!db1.get());
+ EXPECT_TRUE(db1->AddPageData(
+ Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1));
+
+ // Second database with one page.
+ const int kIdee2 = 200802;
+ scoped_ptr<TextDatabase> db2(CreateDB(kIdee2, true, true));
+ ASSERT_TRUE(!!db2.get());
+ EXPECT_TRUE(db2->AddPageData(
+ Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2));
+
+ // Detach, then reattach database one. The file should exist, so we force
+ // opening an existing file.
+ db1.reset();
+ db1.reset(CreateDB(kIdee1, false, false));
+ ASSERT_TRUE(!!db1.get());
+
+ // We should not be able to attach this random database for which no file
+ // exists.
+ const int kIdeeNoExisto = 999999999;
+ scoped_ptr<TextDatabase> db3(CreateDB(kIdeeNoExisto, false, true));
+ EXPECT_FALSE(!!db3.get());
+}
+
+TEST_F(TextDatabaseTest, AddRemove) {
+ // Create a database and add some pages to it.
+ const int kIdee1 = 200801;
+ scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true));
+ ASSERT_TRUE(!!db.get());
+ URLID id1 = db->AddPageData(
+ Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1);
+ EXPECT_NE(0, id1);
+ URLID id2 = db->AddPageData(
+ Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2);
+ EXPECT_NE(0, id2);
+ URLID id3 = db->AddPageData(
+ Time::FromInternalValue(kTime3), kURL3, kTitle3, kBody3);
+ EXPECT_NE(0, id3);
+ EXPECT_EQ(3, RowCount(db.get()));
+
+ // Make sure we can delete some of the data.
+ db->DeletePageData(Time::FromInternalValue(kTime1), kURL1);
+ EXPECT_EQ(2, RowCount(db.get()));
+
+ // Close and reopen.
+ db.reset(new TextDatabase(temp_path_, kIdee1, false));
+ EXPECT_TRUE(db->Init());
+
+ // Verify that the deleted ID is gone and try to delete another one.
+ EXPECT_EQ(2, RowCount(db.get()));
+ db->DeletePageData(Time::FromInternalValue(kTime2), kURL2);
+ EXPECT_EQ(1, RowCount(db.get()));
+}
+
+TEST_F(TextDatabaseTest, Query) {
+ // Make a database with some pages.
+ const int kIdee1 = 200801;
+ scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true));
+ EXPECT_TRUE(!!db.get());
+ AddAllTestData(db.get());
+
+ // Get all the results.
+ QueryOptions options;
+ options.begin_time = Time::FromInternalValue(0);
+
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+ TextDatabase::URLSet unique_urls;
+ db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
+ &first_time_searched);
+ EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs";
+
+ // All 3 sites should be returned in order.
+ ASSERT_EQ(3, results.size());
+ EXPECT_EQ(GURL(kURL1), results[2].url);
+ EXPECT_EQ(GURL(kURL2), results[1].url);
+ EXPECT_EQ(GURL(kURL3), results[0].url);
+
+ // Verify the info on those results.
+ EXPECT_TRUE(Time::FromInternalValue(kTime1) == results[2].time);
+ EXPECT_TRUE(Time::FromInternalValue(kTime2) == results[1].time);
+ EXPECT_TRUE(Time::FromInternalValue(kTime3) == results[0].time);
+
+ EXPECT_EQ(UTF8ToWide(std::string(kTitle1)), results[2].title);
+ EXPECT_EQ(UTF8ToWide(std::string(kTitle2)), results[1].title);
+ EXPECT_EQ(UTF8ToWide(std::string(kTitle3)), results[0].title);
+
+ // Should have no matches in the title.
+ EXPECT_EQ(0, results[0].title_match_positions.size());
+ EXPECT_EQ(0, results[1].title_match_positions.size());
+ EXPECT_EQ(0, results[2].title_match_positions.size());
+
+ // We don't want to be dependent on the exact snippet algorithm, but we know
+ // since we searched for "COUNTTAG" which occurs at the beginning of each
+ // document, that each snippet should start with that.
+ EXPECT_TRUE(StartsWithASCII(WideToUTF8(results[0].snippet.text()),
+ "COUNTTAG", false));
+ EXPECT_TRUE(StartsWithASCII(WideToUTF8(results[1].snippet.text()),
+ "COUNTTAG", false));
+ EXPECT_TRUE(StartsWithASCII(WideToUTF8(results[2].snippet.text()),
+ "COUNTTAG", false));
+}
+
+TEST_F(TextDatabaseTest, TimeRange) {
+ // Make a database with some pages.
+ const int kIdee1 = 200801;
+ scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true));
+ ASSERT_TRUE(!!db.get());
+ AddAllTestData(db.get());
+
+ // Beginning should be inclusive, and the ending exclusive.
+ // Get all the results.
+ QueryOptions options;
+ options.begin_time = Time::FromInternalValue(kTime1);
+ options.end_time = Time::FromInternalValue(kTime3);
+
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+ TextDatabase::URLSet unique_urls;
+ db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
+ &first_time_searched);
+ EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs";
+
+ // The first and second should have been returned.
+ EXPECT_EQ(2, results.size());
+ EXPECT_TRUE(ResultsHaveURL(results, kURL1));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL2));
+ EXPECT_FALSE(ResultsHaveURL(results, kURL3));
+ EXPECT_EQ(kTime1, first_time_searched.ToInternalValue());
+
+ // ---------------------------------------------------------------------------
+ // Do a query where there isn't a result on the begin boundary, so we can
+ // test that the first time searched is set to the minimum time considered
+ // instead of the min value.
+ options.begin_time = Time::FromInternalValue((kTime2 - kTime1) / 2 + kTime1);
+ options.end_time = Time::FromInternalValue(kTime3 + 1);
+ results.clear(); // GetTextMatches does *not* clear the results.
+ db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
+ &first_time_searched);
+ EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs";
+ EXPECT_EQ(options.begin_time.ToInternalValue(),
+ first_time_searched.ToInternalValue());
+
+ // Should have two results, the second and third.
+ EXPECT_EQ(2, results.size());
+ EXPECT_FALSE(ResultsHaveURL(results, kURL1));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL2));
+ EXPECT_TRUE(ResultsHaveURL(results, kURL3));
+
+ // No results should also set the first_time_searched.
+ options.begin_time = Time::FromInternalValue(kTime3 + 1);
+ options.end_time = Time::FromInternalValue(kTime3 * 100);
+ results.clear();
+ db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
+ &first_time_searched);
+ EXPECT_EQ(options.begin_time.ToInternalValue(),
+ first_time_searched.ToInternalValue());
+}
+
+// Make sure that max_count works.
+TEST_F(TextDatabaseTest, MaxCount) {
+ // Make a database with some pages.
+ const int kIdee1 = 200801;
+ scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true));
+ ASSERT_TRUE(!!db.get());
+ AddAllTestData(db.get());
+
+ // Set up the query to return all the results with "Google" (should be 2), but
+ // with a maximum of 1.
+ QueryOptions options;
+ options.begin_time = Time::FromInternalValue(kTime1);
+ options.end_time = Time::FromInternalValue(kTime3 + 1);
+ options.max_count = 1;
+
+ std::vector<TextDatabase::Match> results;
+ Time first_time_searched;
+ TextDatabase::URLSet unique_urls;
+ db->GetTextMatches("google", options, &results, &unique_urls,
+ &first_time_searched);
+ EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs";
+
+ // There should be one result, the most recent one.
+ EXPECT_EQ(1, results.size());
+ EXPECT_TRUE(ResultsHaveURL(results, kURL2));
+
+ // The max time considered should be the date of the returned item.
+ EXPECT_EQ(kTime2, first_time_searched.ToInternalValue());
+}
+
+} // namespace history \ No newline at end of file
diff --git a/chrome/browser/history/thumbnail_database.cc b/chrome/browser/history/thumbnail_database.cc
new file mode 100644
index 0000000..b3d8280
--- /dev/null
+++ b/chrome/browser/history/thumbnail_database.cc
@@ -0,0 +1,475 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/thumbnail_database.h"
+
+#include "base/time.h"
+#include "base/string_util.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/common/jpeg_codec.h"
+#include "chrome/common/sqlite_utils.h"
+#include "chrome/common/thumbnail_score.h"
+#include "skia/include/SkBitmap.h"
+
+namespace history {
+
+// Version number of the database.
+static const int kCurrentVersionNumber = 3;
+
+ThumbnailDatabase::ThumbnailDatabase()
+ : db_(NULL),
+ statement_cache_(NULL),
+ transaction_nesting_(0) {
+}
+
+ThumbnailDatabase::~ThumbnailDatabase() {
+ // The DBCloseScoper will delete the DB and the cache.
+}
+
+InitStatus ThumbnailDatabase::Init(const std::wstring& db_name) {
+ // Open the thumbnail database, using the narrow version of open so that
+ // the DB is in UTF-8.
+ if (sqlite3_open(WideToUTF8(db_name).c_str(), &db_) != SQLITE_OK)
+ return INIT_FAILURE;
+
+ // Set the database page size to something larger to give us
+ // better performance (we're typically seek rather than bandwidth limited).
+ // This only has an effect before any tables have been created, otherwise
+ // this is a NOP. Must be a power of 2 and a max of 8192. We use a bigger
+ // one because we're storing larger data (4-16K) in it, so we want a few
+ // blocks per element.
+ sqlite3_exec(db_, "PRAGMA page_size=4096", NULL, NULL, NULL);
+
+ // The UI is generally designed to work well when the thumbnail database is
+ // slow, so we can tolerate much less caching. The file is also very large
+ // and so caching won't save a significant percentage of it for us,
+ // reducing the benefit of caching in the first place. With the default cache
+ // size of 2000 pages, it will take >8MB of memory, so reducing it can be a
+ // big savings.
+ sqlite3_exec(db_, "PRAGMA cache_size=64", NULL, NULL, NULL);
+
+ // Run the database in exclusive mode. Nobody else should be accessing the
+ // database while we're running, and this will give somewhat improved perf.
+ sqlite3_exec(db_, "PRAGMA locking_mode=EXCLUSIVE", NULL, NULL, NULL);
+
+ statement_cache_ = new SqliteStatementCache;
+ DBCloseScoper scoper(&db_, &statement_cache_);
+
+ // Scope initialization in a transaction so we can't be partially initialized.
+ SQLTransaction transaction(db_);
+ transaction.Begin();
+
+ // Create the tables.
+ if (!meta_table_.Init(std::string(), kCurrentVersionNumber, db_) ||
+ !InitThumbnailTable() ||
+ !InitFavIconsTable(false))
+ return INIT_FAILURE;
+ InitFavIconsIndex();
+
+ // Version check. We should not encounter a database too old for us to handle
+ // in the wild, so we try to continue in that case.
+ if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber)
+ return INIT_TOO_NEW;
+ int cur_version = meta_table_.GetVersionNumber();
+ if (cur_version == 2) {
+ UpgradeToVersion3();
+ cur_version = meta_table_.GetVersionNumber();
+ }
+
+ DLOG_IF(WARNING, cur_version < kCurrentVersionNumber) <<
+ "Thumbnail database version " << cur_version << " is too old for us.";
+
+ // Initialization is complete.
+ if (transaction.Commit() != SQLITE_OK)
+ return INIT_FAILURE;
+
+ // Initialize the statement cache and the scoper to automatically close it.
+ statement_cache_->set_db(db_);
+ scoper.Detach();
+ close_scoper_.Attach(&db_, &statement_cache_);
+
+ // The following code is useful in debugging the thumbnail database. Upon
+ // startup, it will spit out a file for each thumbnail in the database so you
+ // can open them in an external viewer. Insert the path name on your system
+ // into the string below (I recommend using a blank directory since there may
+ // be a lot of files).
+#if 0
+ SQLStatement statement;
+ statement.prepare(db_, "SELECT id, image_data FROM favicons");
+ while (statement.step() == SQLITE_ROW) {
+ int idx = statement.column_int(0);
+ std::vector<unsigned char> data;
+ statement.column_blob_as_vector(1, &data);
+
+ char filename[256];
+ sprintf(filename, "<<< YOUR PATH HERE >>>\\%d.jpeg", idx);
+ FILE* f;
+ if (fopen_s(&f, filename, "wb") == 0) {
+ if (!data.empty())
+ fwrite(&data[0], 1, data.size(), f);
+ fclose(f);
+ }
+ }
+#endif
+
+ return INIT_OK;
+}
+
+bool ThumbnailDatabase::InitThumbnailTable() {
+ if (!DoesSqliteTableExist(db_, "thumbnails")) {
+ if (sqlite3_exec(db_, "CREATE TABLE thumbnails ("
+ "url_id INTEGER PRIMARY KEY,"
+ "boring_score DOUBLE DEFAULT 1.0,"
+ "good_clipping INTEGER DEFAULT 0,"
+ "at_top INTEGER DEFAULT 0,"
+ "last_updated INTEGER DEFAULT 0,"
+ "data BLOB)", NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ }
+ return true;
+}
+
+void ThumbnailDatabase::UpgradeToVersion3() {
+ // sqlite doesn't like the "ALTER TABLE xxx ADD (column_one, two,
+ // three)" syntax, so list out the commands we need to execute:
+ const char* alterations[] = {
+ "ALTER TABLE thumbnails ADD boring_score DOUBLE DEFAULT 1.0",
+ "ALTER TABLE thumbnails ADD good_clipping INTEGER DEFAULT 0",
+ "ALTER TABLE thumbnails ADD at_top INTEGER DEFAULT 0",
+ "ALTER TABLE thumbnails ADD last_updated INTEGER DEFAULT 0",
+ NULL
+ };
+
+ for (int i = 0; alterations[i] != NULL; ++i) {
+ if (sqlite3_exec(db_, alterations[i],
+ NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED() << "Failed to update to v3.";
+ return;
+ }
+ }
+
+ meta_table_.SetVersionNumber(kCurrentVersionNumber);
+}
+
+bool ThumbnailDatabase::RecreateThumbnailTable() {
+ if (sqlite3_exec(db_, "DROP TABLE thumbnails", NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ return InitThumbnailTable();
+}
+
+bool ThumbnailDatabase::InitFavIconsTable(bool is_temporary) {
+ // Note: if you update the schema, don't forget to update
+ // CopyToTemporaryFaviconTable as well.
+ const char* name = is_temporary ? "temp_favicons" : "favicons";
+ if (!DoesSqliteTableExist(db_, name)) {
+ std::string sql;
+ sql.append("CREATE TABLE ");
+ sql.append(name);
+ sql.append("("
+ "id INTEGER PRIMARY KEY,"
+ "url LONGVARCHAR NOT NULL,"
+ "last_updated INTEGER DEFAULT 0,"
+ "image_data BLOB)");
+ if (sqlite3_exec(db_, sql.c_str(), NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ }
+ return true;
+}
+
+void ThumbnailDatabase::InitFavIconsIndex() {
+ // Add an index on the url column. We ignore errors. Since this is always
+ // called during startup, the index will normally already exist.
+ sqlite3_exec(db_, "CREATE INDEX favicons_url ON favicons(url)",
+ NULL, NULL, NULL);
+}
+
+void ThumbnailDatabase::BeginTransaction() {
+ DCHECK(db_);
+ if (transaction_nesting_ == 0) {
+ int rv = sqlite3_exec(db_, "BEGIN TRANSACTION", NULL, NULL, NULL);
+ DCHECK(rv == SQLITE_OK) << "Failed to begin transaction";
+ }
+ transaction_nesting_++;
+}
+
+void ThumbnailDatabase::CommitTransaction() {
+ DCHECK(db_);
+ DCHECK(transaction_nesting_ > 0) << "Committing too many transaction";
+ transaction_nesting_--;
+ if (transaction_nesting_ == 0) {
+ int rv = sqlite3_exec(db_, "COMMIT", NULL, NULL, NULL);
+ DCHECK(rv == SQLITE_OK) << "Failed to commit transaction";
+ }
+}
+
+void ThumbnailDatabase::Vacuum() {
+ DCHECK(transaction_nesting_ == 0) <<
+ "Can not have a transaction when vacuuming.";
+ sqlite3_exec(db_, "VACUUM", NULL, NULL, NULL);
+}
+
+void ThumbnailDatabase::SetPageThumbnail(
+ URLID id,
+ const SkBitmap& thumbnail,
+ const ThumbnailScore& score) {
+ if (!thumbnail.isNull()) {
+ bool add_thumbnail = true;
+ ThumbnailScore current_score;
+ if (ThumbnailScoreForId(id, &current_score)) {
+ add_thumbnail = ShouldReplaceThumbnailWith(current_score, score);
+ }
+
+ if (add_thumbnail) {
+ SQLITE_UNIQUE_STATEMENT(
+ statement, *statement_cache_,
+ "INSERT OR REPLACE INTO thumbnails "
+ "(url_id, boring_score, good_clipping, at_top, last_updated, data) "
+ "VALUES (?,?,?,?,?,?)");
+ if (!statement.is_valid())
+ return;
+
+ // We use 90 quality (out of 100) which is pretty high, because
+ // we're very sensitive to artifacts for these small sized,
+ // highly detailed images.
+ std::vector<unsigned char> jpeg_data;
+ SkAutoLockPixels thumbnail_lock(thumbnail);
+ bool encoded = JPEGCodec::Encode(
+ reinterpret_cast<unsigned char*>(thumbnail.getAddr32(0, 0)),
+ JPEGCodec::FORMAT_BGRA, thumbnail.width(),
+ thumbnail.height(),
+ static_cast<int>(thumbnail.rowBytes()), 90,
+ &jpeg_data);
+
+ if (encoded) {
+ statement->bind_int64(0, id);
+ statement->bind_double(1, score.boring_score);
+ statement->bind_bool(2, score.good_clipping);
+ statement->bind_bool(3, score.at_top);
+ statement->bind_int64(4, score.time_at_snapshot.ToTimeT());
+ statement->bind_blob(5, &jpeg_data[0],
+ static_cast<int>(jpeg_data.size()));
+ if (statement->step() != SQLITE_DONE)
+ DLOG(WARNING) << "Unable to insert thumbnail";
+ }
+ }
+ } else {
+ if ( !DeleteThumbnail(id) )
+ DLOG(WARNING) << "Unable to delete thumbnail";
+ }
+}
+
+bool ThumbnailDatabase::GetPageThumbnail(URLID id,
+ std::vector<unsigned char>* data) {
+ SQLITE_UNIQUE_STATEMENT(
+ statement, *statement_cache_,
+ "SELECT data FROM thumbnails WHERE url_id=?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, id);
+ if (statement->step() != SQLITE_ROW)
+ return false; // don't have a thumbnail for this ID
+
+ return statement->column_blob_as_vector(0, data);
+}
+
+bool ThumbnailDatabase::DeleteThumbnail(URLID id) {
+ SQLITE_UNIQUE_STATEMENT(
+ statement, *statement_cache_,
+ "DELETE FROM thumbnails WHERE url_id = ?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, id);
+ return statement->step() == SQLITE_DONE;
+}
+
+bool ThumbnailDatabase::ThumbnailScoreForId(
+ URLID id,
+ ThumbnailScore* score) {
+ // Fetch the current thumbnail's information to make sure we
+ // aren't replacing a good thumbnail with one that's worse.
+ SQLITE_UNIQUE_STATEMENT(
+ select_statement, *statement_cache_,
+ "SELECT boring_score,good_clipping,at_top,last_updated "
+ "FROM thumbnails WHERE url_id=?");
+ if (!select_statement.is_valid()) {
+ NOTREACHED() << "Couldn't build select statement!";
+ } else {
+ select_statement->bind_int64(0, id);
+ if (select_statement->step() == SQLITE_ROW) {
+ double current_boring_score = select_statement->column_double(0);
+ bool current_clipping = select_statement->column_bool(1);
+ bool current_at_top = select_statement->column_bool(2);
+ Time last_updated = Time::FromTimeT(select_statement->column_int64(3));
+ *score = ThumbnailScore(current_boring_score, current_clipping,
+ current_at_top, last_updated);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool ThumbnailDatabase::SetFavIcon(URLID icon_id,
+ const std::vector<unsigned char>& icon_data,
+ Time time) {
+ DCHECK(icon_id);
+ if (icon_data.size()) {
+ SQLITE_UNIQUE_STATEMENT(
+ statement, *statement_cache_,
+ "UPDATE favicons SET image_data=?,last_updated=? WHERE id=?");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_blob(0, &icon_data.front(),
+ static_cast<int>(icon_data.size()));
+ statement->bind_int64(1, time.ToTimeT());
+ statement->bind_int64(2, icon_id);
+ return statement->step() == SQLITE_DONE;
+ } else {
+ SQLITE_UNIQUE_STATEMENT(
+ statement, *statement_cache_,
+ "UPDATE favicons SET image_data=NULL,last_updated=? WHERE id=?");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_int64(0, time.ToTimeT());
+ statement->bind_int64(1, icon_id);
+ return statement->step() == SQLITE_DONE;
+ }
+}
+
+bool ThumbnailDatabase::SetFavIconLastUpdateTime(FavIconID icon_id,
+ const Time& time) {
+ SQLITE_UNIQUE_STATEMENT(
+ statement, *statement_cache_,
+ "UPDATE favicons SET last_updated=? WHERE id=?");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_int64(0, time.ToTimeT());
+ statement->bind_int64(1, icon_id);
+ return statement->step() == SQLITE_DONE;
+}
+
+FavIconID ThumbnailDatabase::GetFavIconIDForFavIconURL(const GURL& icon_url) {
+ SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
+ "SELECT id FROM favicons WHERE url=?");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_string(0, URLDatabase::GURLToDatabaseURL(icon_url));
+ if (statement->step() != SQLITE_ROW)
+ return 0; // not cached
+
+ return statement->column_int64(0);
+}
+
+bool ThumbnailDatabase::GetFavIcon(
+ FavIconID icon_id,
+ Time* last_updated,
+ std::vector<unsigned char>* png_icon_data,
+ GURL* icon_url) {
+ DCHECK(icon_id);
+
+ SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
+ "SELECT last_updated,image_data,url FROM favicons WHERE id=?");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_int64(0, icon_id);
+
+ if (statement->step() != SQLITE_ROW) {
+ // No entry for the id.
+ return false;
+ }
+
+ *last_updated = Time::FromTimeT(statement->column_int64(0));
+ if (statement->column_bytes(1) > 0)
+ statement->column_blob_as_vector(1, png_icon_data);
+ if (icon_url)
+ *icon_url = GURL(statement->column_text(2));
+
+ return true;
+}
+
+FavIconID ThumbnailDatabase::AddFavIcon(const GURL& icon_url) {
+ SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
+ "INSERT INTO favicons (url) VALUES (?)");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_string(0, URLDatabase::GURLToDatabaseURL(icon_url));
+ if (statement->step() != SQLITE_DONE)
+ return 0;
+ return sqlite3_last_insert_rowid(db_);
+}
+
+bool ThumbnailDatabase::DeleteFavIcon(FavIconID id) {
+ SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
+ "DELETE FROM favicons WHERE id = ?");
+ if (!statement.is_valid())
+ return false;
+ statement->bind_int64(0, id);
+ return statement->step() == SQLITE_DONE;
+}
+
+FavIconID ThumbnailDatabase::CopyToTemporaryFavIconTable(FavIconID source) {
+ SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_,
+ "INSERT INTO temp_favicons("
+ "url, last_updated, image_data)"
+ "SELECT url, last_updated, image_data "
+ "FROM favicons WHERE id = ?");
+ if (!statement.is_valid())
+ return 0;
+ statement->bind_int64(0, source);
+ if (statement->step() != SQLITE_DONE)
+ return 0;
+
+ // We return the ID of the newly inserted favicon.
+ return sqlite3_last_insert_rowid(db_);
+}
+
+bool ThumbnailDatabase::CommitTemporaryFavIconTable() {
+ // Delete the old favicons table.
+ if (sqlite3_exec(db_, "DROP TABLE favicons", NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+
+ // Rename the temporary one.
+ if (sqlite3_exec(db_, "ALTER TABLE temp_favicons RENAME TO favicons",
+ NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+
+ // The renamed table needs the index (the temporary table doesn't have one).
+ InitFavIconsIndex();
+ return true;
+}
+
+} // namespace history
diff --git a/chrome/browser/history/thumbnail_database.h b/chrome/browser/history/thumbnail_database.h
new file mode 100644
index 0000000..ac94283
--- /dev/null
+++ b/chrome/browser/history/thumbnail_database.h
@@ -0,0 +1,189 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_THUMBNAIL_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_THUMBNAIL_DATABASE_H__
+
+#include <vector>
+
+#include "chrome/browser/history/history_database.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/history/url_database.h" // For DBCloseScoper.
+#include "chrome/common/sqlite_compiled_statement.h"
+
+struct sqlite3;
+struct ThumbnailScore;
+class Time;
+
+namespace history {
+
+class ExpireHistoryBackend;
+
+// This database interface is owned by the history backend and runs on the
+// history thread. It is a totally separate component from history partially
+// because we may want to move it to its own thread in the future. The
+// operations we will do on this database will be slow, but we can tolerate
+// higher latency (it's OK for thumbnails to come in slower than the rest
+// of the data). Moving this to a separate thread would not block potentially
+// higher priority history operations.
+class ThumbnailDatabase {
+ public:
+ ThumbnailDatabase();
+ ~ThumbnailDatabase();
+
+ // Must be called after creation but before any other methods are called.
+ // When not INIT_OK, no other functions should be called.
+ InitStatus Init(const std::wstring& db_name);
+
+ // Transactions on the database.
+ void BeginTransaction();
+ void CommitTransaction();
+ int transaction_nesting() const {
+ return transaction_nesting_;
+ }
+
+ // Vacuums the database. This will cause sqlite to defragment and collect
+ // unused space in the file. It can be VERY SLOW.
+ void Vacuum();
+
+ // Thumbnails ----------------------------------------------------------------
+
+ // Sets the given data to be the thumbnail for the given URL,
+ // overwriting any previous data. If the SkBitmap contains no pixel
+ // data, the thumbnail will be deleted.
+ void SetPageThumbnail(URLID id,
+ const SkBitmap& thumbnail,
+ const ThumbnailScore& score);
+
+ // Retrieves thumbnail data for the given URL, returning true on success,
+ // false if there is no such thumbnail or there was some other error.
+ bool GetPageThumbnail(URLID id, std::vector<unsigned char>* data);
+
+ // Delete the thumbnail with the provided id. Returns false on failure
+ bool DeleteThumbnail(URLID id);
+
+ // If there is a thumbnail score for the id provided, retrieves the
+ // current thumbnail score and places it in |score| and returns
+ // true. Returns false otherwise.
+ bool ThumbnailScoreForId(URLID id, ThumbnailScore* score);
+
+ // Called by the to delete all old thumbnails and make a clean table.
+ // Returns true on success.
+ bool RecreateThumbnailTable();
+
+ // FavIcons ------------------------------------------------------------------
+
+ // Sets the bits for a favicon. This should be png encoded data.
+ // The time indicates the access time, and is used to detect when the favicon
+ // should be refreshed.
+ bool SetFavIcon(FavIconID icon_id,
+ const std::vector<unsigned char>& icon_data,
+ Time time);
+
+ // Sets the time the favicon was last updated.
+ bool SetFavIconLastUpdateTime(FavIconID icon_id, const Time& time);
+
+ // Returns the id of the entry in the favicon database with the specified url.
+ // Returns 0 if no entry exists for the specified url.
+ FavIconID GetFavIconIDForFavIconURL(const GURL& icon_url);
+
+ // Gets the png encoded favicon and last updated time for the specified
+ // favicon id.
+ bool GetFavIcon(FavIconID icon_id,
+ Time* last_updated,
+ std::vector<unsigned char>* png_icon_data,
+ GURL* icon_url);
+
+ // Adds the favicon URL to the favicon db, returning its id.
+ FavIconID AddFavIcon(const GURL& icon_url);
+
+ // Delete the favicon with the provided id. Returns false on failure
+ bool DeleteFavIcon(FavIconID id);
+
+ // Temporary FavIcons --------------------------------------------------------
+
+ // Create a temporary table to store favicons. Favicons will be copied to
+ // this table by CopyToTemporaryFavIconTable() and then the original table
+ // will be dropped, leaving only those copied favicons remaining. This is
+ // used to quickly delete most of the favicons when clearing history.
+ bool InitTemporaryFavIconsTable() {
+ return InitFavIconsTable(true);
+ }
+
+ // Copies the given favicon from the "main" favicon table to the temporary
+ // one. This is only valid in between calls to InitTemporaryFavIconsTable()
+ // and CommitTemporaryFavIconTable().
+ //
+ // The ID of the favicon will change when this copy takes place. The new ID
+ // is returned, or 0 on failure.
+ FavIconID CopyToTemporaryFavIconTable(FavIconID source);
+
+ // Replaces the main URL table with the temporary table created by
+ // InitTemporaryFavIconsTable(). This will mean all favicons not copied over
+ // will be deleted. Returns true on success.
+ bool CommitTemporaryFavIconTable();
+
+ private:
+ friend class ExpireHistoryBackend;
+
+ // Creates the thumbnail table, returning true if the table already exists
+ // or was successfully created.
+ bool InitThumbnailTable();
+
+ // Creates the favicon table, returning true if the table already exists,
+ // or was successfully created. is_temporary will be false when generating
+ // the "regular" favicons table. The expirer sets this to true to generate the
+ // temporary table, which will have a different name but the same schema.
+ bool InitFavIconsTable(bool is_temporary);
+
+ // Adds support for the new metadata on web page thumbnails.
+ void UpgradeToVersion3();
+
+ // Creates the index over the favicon table. This will be called during
+ // initialization after the table is created. This is a separate function
+ // because it is used by SwapFaviconTables to create an index over the
+ // newly-renamed favicons table (formerly the temporary table with no index).
+ void InitFavIconsIndex();
+
+ // Ensures that db_ and statement_cache_ are destroyed in the proper order.
+ DBCloseScoper close_scoper_;
+
+ // The database connection and the statement cache: MAY BE NULL if database
+ // init failed. These are cleaned up by the close_scoper_.
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+
+ int transaction_nesting_;
+
+ MetaTableHelper meta_table_;
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_THUMBNAIL_DATABASE_H__
diff --git a/chrome/browser/history/thumbnail_database_unittest.cc b/chrome/browser/history/thumbnail_database_unittest.cc
new file mode 100644
index 0000000..4ede31d
--- /dev/null
+++ b/chrome/browser/history/thumbnail_database_unittest.cc
@@ -0,0 +1,344 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <windows.h>
+
+#include "base/basictypes.h"
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "chrome/browser/history/thumbnail_database.h"
+#include "chrome/common/chrome_paths.h"
+#include "chrome/common/jpeg_codec.h"
+#include "chrome/common/thumbnail_score.h"
+#include "chrome/tools/profiles/thumbnail-inl.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "SkBitmap.h"
+
+namespace history {
+
+namespace {
+
+class ThumbnailDatabaseTest : public testing::Test {
+ public:
+ ThumbnailDatabaseTest() {
+ }
+ ~ThumbnailDatabaseTest() {
+ }
+
+ protected:
+ // testing::Test
+ virtual void SetUp() {
+ // get an empty file for the test DB
+ PathService::Get(chrome::DIR_TEST_DATA, &file_name_);
+ file_name_.push_back(file_util::kPathSeparator);
+ file_name_.append(L"TestThumbnails.db");
+ DeleteFile(file_name_.c_str());
+
+ google_bitmap_.reset(
+ JPEGCodec::Decode(kGoogleThumbnail, sizeof(kGoogleThumbnail)));
+ }
+
+ virtual void TearDown() {
+ DeleteFile(file_name_.c_str());
+ }
+
+ scoped_ptr<SkBitmap> google_bitmap_;
+
+ std::wstring file_name_;
+};
+
+// data we'll put into the thumbnail database
+static const unsigned char blob1[] =
+ "12346102356120394751634516591348710478123649165419234519234512349134";
+static const unsigned char blob2[] =
+ "goiwuegrqrcomizqyzkjalitbahxfjytrqvpqeroicxmnlkhlzunacxaneviawrtxcywhgef";
+static const unsigned char blob3[] =
+ "3716871354098370776510470746794707624107647054607467847164027";
+const double kBoringness = 0.25;
+const double kWorseBoringness = 0.50;
+const double kBetterBoringness = 0.10;
+const double kTotallyBoring = 1.0;
+
+const __int64 kPage1 = 1234;
+
+// converts out constant data above to a vector for SetPageThumbnail
+static std::vector<unsigned char> StringToVector(const unsigned char* str) {
+ size_t len = strlen(reinterpret_cast<const char*>(str));
+ std::vector<unsigned char> vect;
+ vect.resize(len);
+
+ memcpy(&vect[0], str, len);
+ return vect;
+}
+
+} // namespace
+
+
+TEST_F(ThumbnailDatabaseTest, AddDelete) {
+ ThumbnailDatabase db;
+ ASSERT_TRUE(db.Init(file_name_) == INIT_OK);
+
+ // Add one page & verify it got added.
+ ThumbnailScore boring(kBoringness, true, true);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, boring);
+ ThumbnailScore score_output;
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_output));
+ ASSERT_TRUE(boring.Equals(score_output));
+
+ // Verify a random page is not found.
+ __int64 page2 = 5678;
+ std::vector<unsigned char> jpeg_data;
+ EXPECT_FALSE(db.GetPageThumbnail(page2, &jpeg_data));
+ EXPECT_FALSE(db.ThumbnailScoreForId(page2, &score_output));
+
+ // Add another page with a better boringness & verify it got added.
+ ThumbnailScore better_boringness(kBetterBoringness, true, true);
+ db.SetPageThumbnail(page2, *google_bitmap_, better_boringness);
+ ASSERT_TRUE(db.ThumbnailScoreForId(page2, &score_output));
+ ASSERT_TRUE(better_boringness.Equals(score_output));
+
+ // Delete the thumbnail for the second page.
+ ThumbnailScore worse_boringness(kWorseBoringness, true, true);
+ db.SetPageThumbnail(page2, SkBitmap(), worse_boringness);
+ ASSERT_FALSE(db.GetPageThumbnail(page2, &jpeg_data));
+ ASSERT_FALSE(db.ThumbnailScoreForId(page2, &score_output));
+
+ // Delete the first thumbnail using the explicit delete API.
+ ASSERT_TRUE(db.DeleteThumbnail(kPage1));
+
+ // Make sure it is gone
+ ASSERT_FALSE(db.ThumbnailScoreForId(kPage1, &score_output));
+ ASSERT_FALSE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_FALSE(db.ThumbnailScoreForId(page2, &score_output));
+ ASSERT_FALSE(db.GetPageThumbnail(page2, &jpeg_data));
+}
+
+TEST_F(ThumbnailDatabaseTest, UseLessBoringThumbnails) {
+ ThumbnailDatabase db;
+ Time now = Time::Now();
+ ASSERT_TRUE(db.Init(file_name_) == INIT_OK);
+
+ // Add one page & verify it got added.
+ ThumbnailScore boring(kBoringness, true, true);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, boring);
+ std::vector<unsigned char> jpeg_data;
+ ThumbnailScore score_out;
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(boring.Equals(score_out));
+
+ // Attempt to update the first page entry with a thumbnail that
+ // is more boring and verify that it doesn't change.
+ ThumbnailScore more_boring(kWorseBoringness, true, true);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, more_boring);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(boring.Equals(score_out));
+
+ // Attempt to update the first page entry with a thumbnail that
+ // is less boring and verify that we update it.
+ ThumbnailScore less_boring(kBetterBoringness, true, true);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, less_boring);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(less_boring.Equals(score_out));
+}
+
+TEST_F(ThumbnailDatabaseTest, UseAtTopThumbnails) {
+ ThumbnailDatabase db;
+ Time now = Time::Now();
+ ASSERT_TRUE(db.Init(file_name_) == INIT_OK);
+
+ // Add one page & verify it got added. Note that it doesn't have
+ // |good_clipping| and isn't |at_top|.
+ ThumbnailScore boring_and_bad(kBoringness, false, false);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, boring_and_bad);
+ std::vector<unsigned char> jpeg_data;
+ ThumbnailScore score_out;
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(boring_and_bad.Equals(score_out));
+
+ // A thumbnail that's at the top of the page should replace
+ // thumbnails that are in the middle, for the same boringness.
+ ThumbnailScore boring_but_better(kBoringness, false, true);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, boring_but_better);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(boring_but_better.Equals(score_out));
+
+ // The only case where we should replace a thumbnail at the top with
+ // a thumbnail in the middle/bottom is when the current thumbnail is
+ // weirdly stretched and the incoming thumbnail isn't.
+ ThumbnailScore better_boring_bad_framing(kBetterBoringness, false, false);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, better_boring_bad_framing);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(boring_but_better.Equals(score_out));
+
+ ThumbnailScore boring_good_clipping(kBoringness, true, false);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, boring_good_clipping);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(boring_good_clipping.Equals(score_out));
+
+ // Now that we have a non-stretched, middle of the page thumbnail,
+ // we shouldn't be able to replace it with:
+
+ // 1) A stretched thumbnail in the middle of the page
+ db.SetPageThumbnail(kPage1, *google_bitmap_,
+ ThumbnailScore(kBetterBoringness, false, false, now));
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(boring_good_clipping.Equals(score_out));
+
+ // 2) A stretched thumbnail at the top of the page
+ db.SetPageThumbnail(kPage1, *google_bitmap_,
+ ThumbnailScore(kBetterBoringness, false, true, now));
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(boring_good_clipping.Equals(score_out));
+
+ // But it should be replaced by a thumbnail that's clipped properly
+ // and is at the top
+ ThumbnailScore best_score(kBetterBoringness, true, true);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, best_score);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(best_score.Equals(score_out));
+}
+
+TEST_F(ThumbnailDatabaseTest, ThumbnailTimeDegradation) {
+ ThumbnailDatabase db;
+ const Time kNow = Time::Now();
+ const Time kThreeHoursAgo = kNow - TimeDelta::FromHours(4);
+ const Time kFiveHoursAgo = kNow - TimeDelta::FromHours(6);
+ const double kBaseBoringness = 0.305;
+ const double kWorseBoringness = 0.345;
+
+ ASSERT_TRUE(db.Init(file_name_) == INIT_OK);
+
+ // add one page & verify it got added.
+ ThumbnailScore base_boringness(kBaseBoringness, true, true, kFiveHoursAgo);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, base_boringness);
+ std::vector<unsigned char> jpeg_data;
+ ThumbnailScore score_out;
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(base_boringness.Equals(score_out));
+
+ // Try to add a different thumbnail with a worse score an hour later
+ // (but not enough to trip the boringness degradation threshold).
+ ThumbnailScore hour_later(kWorseBoringness, true, true, kThreeHoursAgo);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, hour_later);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(base_boringness.Equals(score_out));
+
+ // After a full five hours, things should have degraded enough
+ // that we'll allow the same thumbnail with the same (worse)
+ // boringness that we previous rejected.
+ ThumbnailScore five_hours_later(kWorseBoringness, true, true, kNow);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, five_hours_later);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(five_hours_later.Equals(score_out));
+}
+
+TEST_F(ThumbnailDatabaseTest, NeverAcceptTotallyBoringThumbnail) {
+ // We enforce a maximum boringness score: even in cases where we
+ // should replace a thumbnail with another because of reasons other
+ // than straight up boringness score, still reject because the
+ // thumbnail is totally boring.
+ ThumbnailDatabase db;
+ Time now = Time::Now();
+ ASSERT_TRUE(db.Init(file_name_) == INIT_OK);
+
+ std::vector<unsigned char> jpeg_data;
+ ThumbnailScore score_out;
+ const double kBaseBoringness = 0.50;
+ const Time kNow = Time::Now();
+ const int kSizeOfTable = 4;
+ struct {
+ bool good_scaling;
+ bool at_top;
+ } const heiarchy_table[] = {
+ {false, false},
+ {false, true},
+ {true, false},
+ {true, true}
+ };
+
+ // Test that for each entry type, all entry types that are better
+ // than it still will reject thumbnails which are totally boring.
+ for (int i = 0; i < kSizeOfTable; ++i) {
+ ThumbnailScore base(kBaseBoringness,
+ heiarchy_table[i].good_scaling,
+ heiarchy_table[i].at_top,
+ kNow);
+
+ db.SetPageThumbnail(kPage1, *google_bitmap_, base);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(base.Equals(score_out));
+
+ for (int j = i; j < kSizeOfTable; ++j) {
+ ThumbnailScore shouldnt_replace(
+ kTotallyBoring, heiarchy_table[j].good_scaling,
+ heiarchy_table[j].at_top, kNow);
+
+ db.SetPageThumbnail(kPage1, *google_bitmap_, shouldnt_replace);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(base.Equals(score_out));
+ }
+
+ // Clean up for the next iteration
+ ASSERT_TRUE(db.DeleteThumbnail(kPage1));
+ ASSERT_FALSE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_FALSE(db.ThumbnailScoreForId(kPage1, &score_out));
+ }
+
+ // We should never accept a totally boring thumbnail no matter how
+ // much old the current thumbnail is.
+ ThumbnailScore base_boring(kBaseBoringness, true, true, kNow);
+ db.SetPageThumbnail(kPage1, *google_bitmap_, base_boring);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(base_boring.Equals(score_out));
+
+ ThumbnailScore totally_boring_in_the_future(
+ kTotallyBoring, true, true, kNow + TimeDelta::FromDays(365));
+ db.SetPageThumbnail(kPage1, *google_bitmap_, totally_boring_in_the_future);
+ ASSERT_TRUE(db.GetPageThumbnail(kPage1, &jpeg_data));
+ ASSERT_TRUE(db.ThumbnailScoreForId(kPage1, &score_out));
+ ASSERT_TRUE(base_boring.Equals(score_out));
+}
+
+} // namespace history
diff --git a/chrome/browser/history/url_database.cc b/chrome/browser/history/url_database.cc
new file mode 100644
index 0000000..0ce8072
--- /dev/null
+++ b/chrome/browser/history/url_database.cc
@@ -0,0 +1,485 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/url_database.h"
+
+#include <algorithm>
+#include <limits>
+
+#include "base/string_util.h"
+#include "chrome/common/l10n_util.h"
+#include "chrome/common/sqlite_utils.h"
+#include "googleurl/src/gurl.h"
+
+namespace history {
+
+const char URLDatabase::kURLRowFields[] = HISTORY_URL_ROW_FIELDS;
+const int URLDatabase::kNumURLRowFields = 9;
+
+bool URLDatabase::URLEnumerator::GetNextURL(URLRow* r) {
+ if (statement_.step() == SQLITE_ROW) {
+ FillURLRow(statement_, r);
+ return true;
+ }
+ return false;
+}
+
+URLDatabase::URLDatabase() : has_keyword_search_terms_(false) {
+}
+
+URLDatabase::~URLDatabase() {
+}
+
+// static
+std::string URLDatabase::GURLToDatabaseURL(const GURL& gurl) {
+ // TODO(brettw): do something fancy here with encoding, etc.
+ return gurl.spec();
+}
+
+// Convenience to fill a history::URLRow. Must be in sync with the fields in
+// kURLRowFields.
+void URLDatabase::FillURLRow(SQLStatement& s, history::URLRow* i) {
+ DCHECK(i);
+ i->id_ = s.column_int64(0);
+ i->url_ = GURL(s.column_text(1));
+ i->title_.assign(s.column_text16(2));
+ i->visit_count_ = s.column_int(3);
+ i->typed_count_ = s.column_int(4);
+ i->last_visit_ = Time::FromInternalValue(s.column_int64(5));
+ i->hidden_ = s.column_int(6) != 0;
+ i->favicon_id_ = s.column_int64(7);
+ i->star_id_ = s.column_int64(8);
+}
+
+bool URLDatabase::GetURLRow(URLID url_id, URLRow* info) {
+ // TODO(brettw) We need check for empty URLs to handle the case where
+ // there are old URLs in the database that are empty that got in before
+ // we added any checks. We should eventually be able to remove it
+ // when all inputs are using GURL (which prohibit empty input).
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls WHERE id=?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, url_id);
+ if (statement->step() == SQLITE_ROW) {
+ FillURLRow(*statement, info);
+ return true;
+ }
+ return false;
+}
+
+URLID URLDatabase::GetRowForURL(const GURL& url, history::URLRow* info) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls WHERE url=?");
+ if (!statement.is_valid())
+ return 0;
+
+ std::string url_string = GURLToDatabaseURL(url);
+ statement->bind_string(0, url_string);
+ if (statement->step() != SQLITE_ROW)
+ return 0; // no data
+
+ if (info)
+ FillURLRow(*statement, info);
+ return statement->column_int64(0);
+}
+
+bool URLDatabase::UpdateURLRow(URLID url_id,
+ const history::URLRow& info) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "UPDATE urls SET title=?,visit_count=?,typed_count=?,last_visit_time=?,"
+ "hidden=?,starred_id=?,favicon_id=?"
+ "WHERE id=?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_wstring(0, info.title());
+ statement->bind_int(1, info.visit_count());
+ statement->bind_int(2, info.typed_count());
+ statement->bind_int64(3, info.last_visit().ToInternalValue());
+ statement->bind_int(4, info.hidden() ? 1 : 0);
+ statement->bind_int64(5, info.star_id());
+ statement->bind_int64(6, info.favicon_id());
+ statement->bind_int64(7, url_id);
+ return statement->step() == SQLITE_DONE;
+}
+
+URLID URLDatabase::AddURLInternal(const history::URLRow& info,
+ bool is_temporary) {
+ // This function is used to insert into two different tables, so we have to
+ // do some shuffling. Unfortinately, we can't use the macro
+ // HISTORY_URL_ROW_FIELDS because that specifies the table name which is
+ // invalid in the insert syntax.
+ #define ADDURL_COMMON_SUFFIX \
+ "(url,title,visit_count,typed_count,"\
+ "last_visit_time,hidden,starred_id,favicon_id)"\
+ "VALUES(?,?,?,?,?,?,?,?)"
+ const char* statement_name;
+ const char* statement_sql;
+ if (is_temporary) {
+ statement_name = "AddURLTemporary";
+ statement_sql = "INSERT INTO temp_urls" ADDURL_COMMON_SUFFIX;
+ } else {
+ statement_name = "AddURL";
+ statement_sql = "INSERT INTO urls" ADDURL_COMMON_SUFFIX;
+ }
+ #undef ADDURL_COMMON_SUFFIX
+
+ SqliteCompiledStatement statement(statement_name, 0, GetStatementCache(),
+ statement_sql);
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_string(0, GURLToDatabaseURL(info.url()));
+ statement->bind_wstring(1, info.title());
+ statement->bind_int(2, info.visit_count());
+ statement->bind_int(3, info.typed_count());
+ statement->bind_int64(4, info.last_visit().ToInternalValue());
+ statement->bind_int(5, info.hidden() ? 1 : 0);
+ statement->bind_int64(6, info.star_id());
+ statement->bind_int64(7, info.favicon_id());
+
+ if (statement->step() != SQLITE_DONE)
+ return 0;
+ return sqlite3_last_insert_rowid(GetDB());
+}
+
+bool URLDatabase::DeleteURLRow(URLID id) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "DELETE FROM urls WHERE id = ?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, id);
+ if (statement->step() != SQLITE_DONE)
+ return false;
+
+ // And delete any keyword visits.
+ if (!has_keyword_search_terms_)
+ return true;
+
+ SQLITE_UNIQUE_STATEMENT(del_keyword_visit, GetStatementCache(),
+ "DELETE FROM keyword_search_terms WHERE url_id=?");
+ if (!del_keyword_visit.is_valid())
+ return false;
+ del_keyword_visit->bind_int64(0, id);
+ return (del_keyword_visit->step() == SQLITE_DONE);
+}
+
+bool URLDatabase::CreateTemporaryURLTable() {
+ return CreateURLTable(true);
+}
+
+bool URLDatabase::CommitTemporaryURLTable() {
+ // See the comments in the header file as well as
+ // HistoryBackend::DeleteAllHistory() for more information on how this works
+ // and why it does what it does.
+ //
+ // Note that the main database overrides this to additionally create the
+ // supplimentary indices that the archived database doesn't need.
+
+ // Swap the url table out and replace it with the temporary one.
+ if (sqlite3_exec(GetDB(), "DROP TABLE urls",
+ NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED();
+ return false;
+ }
+ if (sqlite3_exec(GetDB(), "ALTER TABLE temp_urls RENAME TO urls",
+ NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED();
+ return false;
+ }
+
+ // Create the index over URLs. This is needed for the main, in-memory, and
+ // archived databases, so we always do it. The supplimentary indices used by
+ // the main database are not created here. When deleting all history, they
+ // are created by HistoryDatabase::RecreateAllButStarAndURLTables().
+ CreateMainURLIndex();
+
+ return true;
+}
+
+bool URLDatabase::InitURLEnumeratorForEverything(URLEnumerator* enumerator) {
+ DCHECK(!enumerator->initialized_);
+ std::string sql("SELECT ");
+ sql.append(kURLRowFields);
+ sql.append(" FROM urls");
+ if (enumerator->statement_.prepare(GetDB(), sql.c_str()) != SQLITE_OK) {
+ NOTREACHED() << "Query statement prep failed";
+ return false;
+ }
+ enumerator->initialized_ = true;
+ return true;
+}
+
+bool URLDatabase::IsFavIconUsed(FavIconID favicon_id) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT id FROM urls WHERE favicon_id=? LIMIT 1");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, favicon_id);
+ return statement->step() == SQLITE_ROW;
+}
+
+void URLDatabase::AutocompleteForPrefix(const std::wstring& prefix,
+ size_t max_results,
+ std::vector<history::URLRow>* results) {
+ results->clear();
+
+ // NOTE: Sorting by "starred_id == 0" is subtle. First we do the comparison,
+ // and, according to the SQLite docs, get a numeric result (all binary
+ // operators except "||" result in a numeric value), which is either 1 or 0
+ // (implied by documentation showing the results of various comparison
+ // operations to always be 1 or 0). Now we sort ascending, meaning all 0s go
+ // first -- so, all URLs where "starred_id == 0" is 0, i.e. all starred URLs.
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_URL_ROW_FIELDS "FROM urls "
+ "WHERE url >= ? AND url < ? AND hidden = 0 "
+ "ORDER BY typed_count DESC, starred_id == 0, visit_count DESC, "
+ "last_visit_time DESC "
+ "LIMIT ?");
+ if (!statement.is_valid())
+ return;
+
+ // We will find all strings between "prefix" and this string, which is prefix
+ // followed by the maximum character size.
+ std::wstring end_query(prefix);
+ end_query.push_back(std::numeric_limits<wchar_t>::max());
+
+ statement->bind_wstring(0, prefix);
+ statement->bind_wstring(1, end_query);
+ statement->bind_int(2, static_cast<int>(max_results));
+
+ while (statement->step() == SQLITE_ROW) {
+ history::URLRow info;
+ FillURLRow(*statement, &info);
+ if (info.url().is_valid())
+ results->push_back(info);
+ }
+}
+
+bool URLDatabase::FindShortestURLFromBase(const std::string& base,
+ const std::string& url,
+ int min_visits,
+ int min_typed,
+ bool allow_base,
+ history::URLRow* info) {
+ // Select URLs that start with |base| and are prefixes of |url|. All parts
+ // of this query except the substr() call can be done using the index. We
+ // could do this query with a couple of LIKE or GLOB statements as well, but
+ // those wouldn't use the index, and would run into problems with "wildcard"
+ // characters that appear in URLs (% for LIKE, or *, ? for GLOB).
+ std::string sql("SELECT ");
+ sql.append(kURLRowFields);
+ sql.append(" FROM urls WHERE url ");
+ sql.append(allow_base ? ">=" : ">");
+ sql.append(" ? AND url < :end AND url = substr(:end, 1, length(url)) "
+ "AND hidden = 0 AND visit_count >= ? AND typed_count >= ? "
+ "ORDER BY url LIMIT 1");
+ SQLStatement statement;
+ if (statement.prepare(GetDB(), sql.c_str()) != SQLITE_OK) {
+ NOTREACHED() << "select statement prep failed";
+ return false;
+ }
+
+ statement.bind_string(0, base);
+ statement.bind_string(1, url); // :end
+ statement.bind_int(2, min_visits);
+ statement.bind_int(3, min_typed);
+
+ if (statement.step() != SQLITE_ROW)
+ return false;
+
+ DCHECK(info);
+ FillURLRow(statement, info);
+ return true;
+}
+
+bool URLDatabase::InitKeywordSearchTermsTable() {
+ has_keyword_search_terms_ = true;
+ if (!DoesSqliteTableExist(GetDB(), "keyword_search_terms")) {
+ if (sqlite3_exec(GetDB(), "CREATE TABLE keyword_search_terms ("
+ "keyword_id INTEGER NOT NULL," // ID of the TemplateURL.
+ "url_id INTEGER NOT NULL," // ID of the url.
+ "lower_term LONGVARCHAR NOT NULL," // The search term, in lower case.
+ "term LONGVARCHAR NOT NULL)", // The actual search term.
+ NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ }
+
+ // For searching.
+ sqlite3_exec(GetDB(), "CREATE INDEX keyword_search_terms_index1 ON "
+ "keyword_search_terms (keyword_id, lower_term)",
+ NULL, NULL, NULL);
+
+ // For deletion.
+ sqlite3_exec(GetDB(), "CREATE INDEX keyword_search_terms_index2 ON "
+ "keyword_search_terms (url_id)",
+ NULL, NULL, NULL);
+
+ return true;
+}
+
+bool URLDatabase::DropKeywordSearchTermsTable() {
+ // This will implicitly delete the indices over the table.
+ return sqlite3_exec(GetDB(), "DROP TABLE keyword_search_terms",
+ NULL, NULL, NULL) == SQLITE_OK;
+}
+
+bool URLDatabase::SetKeywordSearchTermsForURL(URLID url_id,
+ TemplateURL::IDType keyword_id,
+ const std::wstring& term) {
+ DCHECK(url_id && keyword_id && !term.empty());
+
+ SQLITE_UNIQUE_STATEMENT(exist_statement, GetStatementCache(),
+ "SELECT term FROM keyword_search_terms "
+ "WHERE keyword_id = ? AND url_id = ?");
+ if (!exist_statement.is_valid())
+ return false;
+ exist_statement->bind_int64(0, keyword_id);
+ exist_statement->bind_int64(1, url_id);
+ if (exist_statement->step() == SQLITE_ROW)
+ return true; // Term already exists, no need to add it.
+
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "INSERT INTO keyword_search_terms (keyword_id, url_id, lower_term, term) "
+ "VALUES (?,?,?,?)");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, keyword_id);
+ statement->bind_int64(1, url_id);
+ statement->bind_wstring(2, l10n_util::ToLower(term));
+ statement->bind_wstring(3, term);
+ return (statement->step() == SQLITE_DONE);
+}
+
+void URLDatabase::DeleteAllSearchTermsForKeyword(
+ TemplateURL::IDType keyword_id) {
+ DCHECK(keyword_id);
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "DELETE FROM keyword_search_terms WHERE keyword_id=?");
+ if (!statement.is_valid())
+ return;
+
+ statement->bind_int64(0, keyword_id);
+ statement->step();
+}
+
+void URLDatabase::GetMostRecentKeywordSearchTerms(
+ TemplateURL::IDType keyword_id,
+ const std::wstring& prefix,
+ int max_count,
+ std::vector<KeywordSearchTermVisit>* matches) {
+ // NOTE: the keyword_id can be zero if on first run the user does a query
+ // before the TemplateURLModel has finished loading. As the chances of this
+ // occurring are small, we ignore it.
+ if (!keyword_id)
+ return;
+
+ DCHECK(!prefix.empty());
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT DISTINCT kv.term, u.last_visit_time "
+ "FROM keyword_search_terms kv "
+ "JOIN urls u ON kv.url_id = u.id "
+ "WHERE kv.keyword_id = ? AND kv.lower_term >= ? AND kv.lower_term < ? "
+ "ORDER BY u.last_visit_time DESC LIMIT ?");
+ if (!statement.is_valid())
+ return;
+
+ // NOTE: Keep this ToLower() call in sync with search_provider.cc.
+ const std::wstring lower_prefix = l10n_util::ToLower(prefix);
+ // This magic gives us a prefix search.
+ std::wstring next_prefix = lower_prefix;
+ next_prefix[next_prefix.size() - 1] =
+ next_prefix[next_prefix.size() - 1] + 1;
+ statement->bind_int64(0, keyword_id);
+ statement->bind_wstring(1, lower_prefix);
+ statement->bind_wstring(2, next_prefix);
+ statement->bind_int(3, max_count);
+
+ KeywordSearchTermVisit visit;
+ while (statement->step() == SQLITE_ROW) {
+ visit.term = statement->column_string16(0);
+ visit.time = Time::FromInternalValue(statement->column_int64(1));
+ matches->push_back(visit);
+ }
+}
+
+bool URLDatabase::MigrateFromVersion11ToVersion12() {
+ URLRow about_row;
+ if (GetRowForURL(GURL("about:blank"), &about_row)) {
+ about_row.set_favicon_id(0);
+ return UpdateURLRow(about_row.id(), about_row);
+ }
+ return true;
+}
+
+bool URLDatabase::CreateURLTable(bool is_temporary) {
+ const char* name = is_temporary ? "temp_urls" : "urls";
+ if (DoesSqliteTableExist(GetDB(), name))
+ return true;
+
+ std::string sql;
+ sql.append("CREATE TABLE ");
+ sql.append(name);
+ sql.append("("
+ "id INTEGER PRIMARY KEY,"
+ "url LONGVARCHAR,"
+ "title LONGVARCHAR,"
+ "visit_count INTEGER DEFAULT 0 NOT NULL,"
+ "typed_count INTEGER DEFAULT 0 NOT NULL,"
+ "last_visit_time INTEGER NOT NULL,"
+ "hidden INTEGER DEFAULT 0 NOT NULL,"
+ "favicon_id INTEGER DEFAULT 0 NOT NULL,"
+ "starred_id INTEGER DEFAULT 0 NOT NULL)");
+
+ return sqlite3_exec(GetDB(), sql.c_str(), NULL, NULL, NULL) == SQLITE_OK;
+}
+
+void URLDatabase::CreateMainURLIndex() {
+ // Index over URLs so we can quickly look up based on URL. Ignore errors as
+ // this likely already exists (and the same below).
+ sqlite3_exec(GetDB(), "CREATE INDEX urls_url_index ON urls (url)", NULL, NULL,
+ NULL);
+}
+
+void URLDatabase::CreateSupplimentaryURLIndices() {
+ // Add a favicon index. This is useful when we delete urls.
+ sqlite3_exec(GetDB(), "CREATE INDEX urls_favicon_id_INDEX ON urls (favicon_id)",
+ NULL, NULL, NULL);
+
+ // Index on starred_id.
+ sqlite3_exec(GetDB(), "CREATE INDEX urls_starred_id_INDEX ON urls "
+ "(starred_id)", NULL, NULL, NULL);
+}
+
+} // namespace history
diff --git a/chrome/browser/history/url_database.h b/chrome/browser/history/url_database.h
new file mode 100644
index 0000000..300c05b
--- /dev/null
+++ b/chrome/browser/history/url_database.h
@@ -0,0 +1,326 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_URL_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_URL_DATABASE_H__
+
+#include "base/basictypes.h"
+#include "chrome/browser/history/history_types.h"
+#include "chrome/browser/template_url.h"
+
+// Temporary until DBCloseScoper moves elsewhere.
+#include "chrome/common/sqlite_compiled_statement.h"
+
+class GURL;
+struct sqlite3;
+class SqliteStatementCache;
+
+namespace history {
+
+class VisitDatabase; // For friend statement.
+
+// This class is here temporarily.
+// TODO(brettw) Figure out a better place for this or obsolete it.
+//
+// Helper class that closes the DB, deletes the statement cache, and zeros out
+// the pointer when it goes out of scope, does nothing once success is called.
+//
+// Can either be used by the owner of the DB to automatically close it, or
+// during initialization so that it is automatically closed on failure.
+//
+// properly maintained by Init() on failure. If |statement_cache| is non NULL,
+// it is assumed to be bound with |db| and will be cleaned up before the
+// database is closed.
+class DBCloseScoper {
+ public:
+ DBCloseScoper() : db_(NULL), statement_cache_(NULL) {
+ }
+
+ // The statement cache must be allocated on the heap. The DB must be
+ // allocated by sqlite.
+ DBCloseScoper(sqlite3** db, SqliteStatementCache** statement_cache)
+ : db_(db),
+ statement_cache_(statement_cache) {
+ }
+
+ ~DBCloseScoper() {
+ if (db_) {
+ if (*statement_cache_) {
+ delete *statement_cache_;
+ *statement_cache_ = NULL;
+ }
+
+ sqlite3_close(*db_);
+ *db_ = NULL;
+ }
+ }
+
+ void Attach(sqlite3** db, SqliteStatementCache** statement_cache) {
+ DCHECK(db_ == NULL && statement_cache_ == NULL);
+ db_ = db;
+ statement_cache_ = statement_cache;
+ }
+
+ void Detach() {
+ db_ = NULL;
+ statement_cache_ = NULL;
+ }
+
+ private:
+ sqlite3** db_;
+ SqliteStatementCache** statement_cache_;
+};
+
+// Encapsulates an SQL database that holds URL info. This is a subset of the
+// full history data. We split this class' functionality out from the larger
+// HistoryDatabase class to support maintaining separate databases of URLs with
+// different capabilities (for example, in-memory, or archived).
+//
+// This is refcounted to support calling InvokeLater() with some of its methods
+// (necessary to maintain ordering of DB operations).
+class URLDatabase {
+ public:
+ // Must call CreateURLTable() and CreateURLIndexes() before using to make
+ // sure the database is initialized.
+ URLDatabase();
+
+ // This object must be destroyed on the thread where all accesses are
+ // happening to avoid thread-safety problems.
+ virtual ~URLDatabase();
+
+ // Converts a GURL to a string used in the history database. We plan to
+ // do more complex operations than just getting the spec out involving
+ // punycode, so this function should be used instead of url.spec() when
+ // interacting with the database.
+ //
+ // TODO(brettw) this should be moved out of the public section and the
+ // entire public HistoryDatabase interface should use GURL. This should
+ // also probably return a string instead since that is what the DB uses
+ // internally and we can avoid the extra conversion.
+ static std::string GURLToDatabaseURL(const GURL& url);
+
+ // URL table functions -------------------------------------------------------
+
+ // Looks up a url given an id. Fills info with the data. Returns true on
+ // success and false otherwise.
+ bool GetURLRow(URLID url_id, URLRow* info);
+
+ // Looks up the given URL and if it exists, fills the given pointers with the
+ // associated info and returns the ID of that URL. If the info pointer is
+ // NULL, no information about the URL will be filled in, only the ID will be
+ // returned. Returns 0 if the URL was not found.
+ URLID GetRowForURL(const GURL& url, URLRow* info);
+
+ // Given an already-existing row in the URL table, updates that URL's stats.
+ // This can not change the URL. Returns true on success.
+ //
+ // This will NOT update the title used for full text indexing. If you are
+ // setting the title, call SetPageIndexedData with the new title.
+ bool UpdateURLRow(URLID url_id, const URLRow& info);
+
+ // Adds a line to the URL database with the given information and returns the
+ // row ID. A row with the given URL must not exist. Returns 0 on error.
+ //
+ // This does NOT add a row to the full text search database. Use
+ // HistoryDatabase::SetPageIndexedData to do this.
+ URLID AddURL(const URLRow& info) {
+ return AddURLInternal(info, false);
+ }
+
+ // Delete the row of the corresponding URL. Only the row in the URL table
+ // will be deleted, not any other data that may refer to it. Returns true if
+ // the row existed and was deleted.
+ bool DeleteURLRow(URLID id);
+
+ // URL mass-deleting ---------------------------------------------------------
+
+ // Begins the mass-deleting operation by creating a temporary URL table.
+ // The caller than adds the URLs it wants to preseve to the temporary table,
+ // and then deletes everything else by calling CommitTemporaryURLTable().
+ // Returns true on success.
+ bool CreateTemporaryURLTable();
+
+ // Adds a row to the temporary URL table. This must be called between
+ // CreateTemporaryURLTable() and CommitTemporaryURLTable() (see those for more
+ // info). The ID of the URL will change in the temporary table, so the new ID
+ // is returned. Returns 0 on failure.
+ URLID AddTemporaryURL(const URLRow& row) {
+ return AddURLInternal(row, true);
+ }
+
+ // Ends the mass-deleting by replacing the original URL table with the
+ // temporary one created in CreateTemporaryURLTable. Returns true on success.
+ //
+ // This function does not create the supplimentary indices. It is virtual so
+ // that the main history database can provide this additional behavior.
+ virtual bool CommitTemporaryURLTable();
+
+ // Enumeration ---------------------------------------------------------------
+
+ // A basic enumerator to enumerate urls
+ class URLEnumerator {
+ public:
+ URLEnumerator() : initialized_(false) {
+ }
+
+ // Retreives the next url. Returns false if no more urls are available
+ bool GetNextURL(history::URLRow* r);
+
+ private:
+ friend class URLDatabase;
+
+ bool initialized_;
+ SQLStatement statement_;
+ };
+
+ // Initializes the given enumerator to enumerator all URLs in the database
+ bool InitURLEnumeratorForEverything(URLEnumerator* enumerator);
+
+ // Favicons ------------------------------------------------------------------
+
+ // Check whether a favicon is used by any URLs in the database.
+ bool IsFavIconUsed(FavIconID favicon_id);
+
+ // Autocomplete --------------------------------------------------------------
+
+ // Fills the given array with URLs matching the given prefix. They will be
+ // sorted by typed count, then by visit count, then by visit date (most
+ // recent first) up to the given maximum number. Called by HistoryURLProvider.
+ void AutocompleteForPrefix(const std::wstring& prefix,
+ size_t max_results,
+ std::vector<URLRow>* results);
+
+ // Tries to find the shortest URL beginning with |base| that strictly
+ // prefixes |url|, and has minimum visit_ and typed_counts as specified.
+ // If found, fills in |info| and returns true; otherwise returns false,
+ // leaving |info| unchanged.
+ // We allow matches of exactly |base| iff |allow_base| is true.
+ bool FindShortestURLFromBase(const std::string& base,
+ const std::string& url,
+ int min_visits,
+ int min_typed,
+ bool allow_base,
+ history::URLRow* info);
+
+ // Keyword Search Terms ------------------------------------------------------
+
+ // Sets the search terms for the specified url/keyword pair.
+ bool SetKeywordSearchTermsForURL(URLID url_id,
+ TemplateURL::IDType keyword_id,
+ const std::wstring& term);
+
+ // Deletes all search terms for the specified keyword that have been added by
+ // way of SetKeywordSearchTermsForURL.
+ void DeleteAllSearchTermsForKeyword(TemplateURL::IDType keyword_id);
+
+ // Returns up to max_count of the most recent search terms for the specified
+ // keyword.
+ void GetMostRecentKeywordSearchTerms(
+ TemplateURL::IDType keyword_id,
+ const std::wstring& prefix,
+ int max_count,
+ std::vector<KeywordSearchTermVisit>* matches);
+
+ // Do to a bug we were setting the favicon of about:blank. This forces
+ // about:blank to have no icon or title. Returns true on success, false if
+ // the favicon couldn't be updated.
+ bool MigrateFromVersion11ToVersion12();
+
+ protected:
+ friend VisitDatabase;
+
+ // See HISTORY_URL_ROW_FIELDS below.
+ static const char kURLRowFields[];
+
+ // The number of fiends in kURLRowFields. If callers need additional
+ // fields, they can add their 0-based index to this value to get the index of
+ // fields following kURLRowFields.
+ static const int kNumURLRowFields;
+
+ // Initialization functions. The indexing functions are separate from the
+ // table creation functions so the in-memory database and the temporary tables
+ // used when clearing history can populate the table and then create the
+ // index, which is faster than the reverse.
+ //
+ // is_temporary is false when generating the "regular" URLs table. The expirer
+ // sets this to true to generate the temporary table, which will have a
+ // different name but the same schema.
+ bool CreateURLTable(bool is_temporary);
+ // We have two tiers of indices for the URL table. The main tier is used by
+ // all URL databases, and is an index over the URL itself. The main history
+ // DB also creates indices over the favicons and bookmark IDs. The archived
+ // and in-memory databases don't need these supplimentary indices so we can
+ // save space by not creating them.
+ void CreateMainURLIndex();
+ void CreateSupplimentaryURLIndices();
+
+ // Ensures the keyword search terms table exists.
+ bool InitKeywordSearchTermsTable();
+
+ // Deletes the keyword search terms table.
+ bool DropKeywordSearchTermsTable();
+
+ // Inserts the given URL row into the URLs table, using the regular table
+ // if is_temporary is false, or the temporary URL table if is temporary is
+ // true. The temporary table may only be used in between
+ // CreateTemporaryURLTable() and CommitTemporaryURLTable().
+ URLID AddURLInternal(const URLRow& info, bool is_temporary);
+
+ // Convenience to fill a history::URLRow. Must be in sync with the fields in
+ // kHistoryURLRowFields.
+ static void FillURLRow(SQLStatement& s, URLRow* i);
+
+ // Returns the database and statement cache for the functions in this
+ // interface. The decendent of this class implements these functions to
+ // return its objects.
+ virtual sqlite3* GetDB() = 0;
+ virtual SqliteStatementCache& GetStatementCache() = 0;
+
+ private:
+ // True if InitKeywordSearchTermsTable() has been invoked. Not all subclasses
+ // have keyword search terms.
+ bool has_keyword_search_terms_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(URLDatabase);
+};
+
+// The fields and order expected by FillURLRow(). ID is guaranteed to be first
+// so that DISTINCT can be prepended to get distinct URLs.
+//
+// This is available BOTH as a macro and a static string (kURLRowFields). Use
+// the macro if you want to put this in the middle of an otherwise constant
+// string, it will save time doing string appends. If you have to build a SQL
+// string dynamically anyway, use the constant, it will save space.
+#define HISTORY_URL_ROW_FIELDS \
+ " urls.id, urls.url, urls.title, urls.visit_count, urls.typed_count, " \
+ "urls.last_visit_time, urls.hidden, urls.favicon_id, urls.starred_id "
+
+} // history
+
+#endif // CHROME_BROWSER_HISTORY_URL_DATABASE_H__
diff --git a/chrome/browser/history/url_database_unittest.cc b/chrome/browser/history/url_database_unittest.cc
new file mode 100644
index 0000000..7f2bdae
--- /dev/null
+++ b/chrome/browser/history/url_database_unittest.cc
@@ -0,0 +1,207 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/string_util.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/common/sqlite_compiled_statement.h"
+#include "chrome/common/sqlite_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace history {
+
+namespace {
+
+bool IsURLRowEqual(const URLRow& a,
+ const URLRow& b) {
+ // TODO(brettw) when the database stores an actual Time value rather than
+ // a time_t, do a reaul comparison. Instead, we have to do a more rough
+ // comparison since the conversion reduces the precision.
+ return a.title() == b.title() &&
+ a.visit_count() == b.visit_count() &&
+ a.typed_count() == b.typed_count() &&
+ a.last_visit() - b.last_visit() <= TimeDelta::FromSeconds(1) &&
+ a.hidden() == b.hidden() &&
+ a.starred() == b.starred();
+}
+
+class URLDatabaseTest : public testing::Test,
+ public URLDatabase {
+ public:
+ URLDatabaseTest() : db_(NULL), statement_cache_(NULL) {
+ }
+
+ private:
+ // Test setup.
+ void SetUp() {
+ PathService::Get(base::DIR_TEMP, &db_file_);
+ db_file_.push_back(file_util::kPathSeparator);
+ db_file_.append(L"URLTest.db");
+
+ EXPECT_EQ(SQLITE_OK, sqlite3_open(WideToUTF8(db_file_).c_str(), &db_));
+ statement_cache_ = new SqliteStatementCache(db_);
+
+ // Initialize the tables for this test.
+ CreateURLTable(false);
+ CreateMainURLIndex();
+ CreateSupplimentaryURLIndices();
+ InitKeywordSearchTermsTable();
+ }
+ void TearDown() {
+ delete statement_cache_;
+ sqlite3_close(db_);
+ file_util::Delete(db_file_, false);
+ }
+
+ // Provided for URL/VisitDatabase.
+ virtual sqlite3* GetDB() {
+ return db_;
+ }
+ virtual SqliteStatementCache& GetStatementCache() {
+ return *statement_cache_;
+ }
+
+ std::wstring db_file_;
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+};
+
+} // namespace
+
+// Test add and query for the URL table in the HistoryDatabase
+TEST_F(URLDatabaseTest, AddURL) {
+ // first, add two URLs
+ const GURL url1(L"http://www.google.com/");
+ URLRow url_info1(url1);
+ url_info1.set_title(L"Google");
+ url_info1.set_visit_count(4);
+ url_info1.set_typed_count(2);
+ url_info1.set_last_visit(Time::Now() - TimeDelta::FromDays(1));
+ url_info1.set_hidden(false);
+ EXPECT_TRUE(AddURL(url_info1));
+
+ const GURL url2(L"http://mail.google.com/");
+ URLRow url_info2(url2);
+ url_info2.set_title(L"Google Mail");
+ url_info2.set_visit_count(3);
+ url_info2.set_typed_count(0);
+ url_info2.set_last_visit(Time::Now() - TimeDelta::FromDays(2));
+ url_info2.set_hidden(true);
+ EXPECT_TRUE(AddURL(url_info2));
+
+ // query both of them
+ URLRow info;
+ EXPECT_TRUE(GetRowForURL(url1, &info));
+ EXPECT_TRUE(IsURLRowEqual(url_info1, info));
+ URLID id2 = GetRowForURL(url2, &info);
+ EXPECT_TRUE(id2);
+ EXPECT_TRUE(IsURLRowEqual(url_info2, info));
+
+ // update the second
+ url_info2.set_title(L"Google Mail Too");
+ url_info2.set_visit_count(4);
+ url_info2.set_typed_count(1);
+ url_info2.set_typed_count(91011);
+ url_info2.set_hidden(false);
+ EXPECT_TRUE(UpdateURLRow(id2, url_info2));
+
+ // make sure it got updated
+ URLRow info2;
+ EXPECT_TRUE(GetRowForURL(url2, &info2));
+ EXPECT_TRUE(IsURLRowEqual(url_info2, info2));
+
+ // query a nonexistant URL
+ EXPECT_EQ(0, GetRowForURL(GURL("http://news.google.com/"), &info));
+
+ // Delete all urls in the domain
+ // FIXME(ACW) test the new url based delete domain
+ //EXPECT_TRUE(db.DeleteDomain(kDomainID));
+
+ // Make sure the urls have been properly removed
+ // FIXME(ACW) commented out because remove no longer works.
+ //EXPECT_TRUE(db.GetURLInfo(url1, NULL) == NULL);
+ //EXPECT_TRUE(db.GetURLInfo(url2, NULL) == NULL);
+}
+
+// Tests adding, querying and deleting keyword visits.
+TEST_F(URLDatabaseTest, KeywordSearchTermVisit) {
+ const GURL url1(L"http://www.google.com/");
+ URLRow url_info1(url1);
+ url_info1.set_title(L"Google");
+ url_info1.set_visit_count(4);
+ url_info1.set_typed_count(2);
+ url_info1.set_last_visit(Time::Now() - TimeDelta::FromDays(1));
+ url_info1.set_hidden(false);
+ URLID url_id = AddURL(url_info1);
+ ASSERT_TRUE(url_id != 0);
+
+ // Add a keyword visit.
+ ASSERT_TRUE(SetKeywordSearchTermsForURL(url_id, 1, L"visit"));
+
+ // Make sure we get it back.
+ std::vector<KeywordSearchTermVisit> matches;
+ GetMostRecentKeywordSearchTerms(1, L"visit", 10, &matches);
+ ASSERT_EQ(1, matches.size());
+ ASSERT_EQ(L"visit", matches[0].term);
+
+ // Delete the keyword visit.
+ DeleteAllSearchTermsForKeyword(1);
+
+ // Make sure we don't get it back when querying.
+ matches.clear();
+ GetMostRecentKeywordSearchTerms(1, L"visit", 10, &matches);
+ ASSERT_EQ(0, matches.size());
+}
+
+// Make sure deleting a URL also deletes a keyword visit.
+TEST_F(URLDatabaseTest, DeleteURLDeletesKeywordSearchTermVisit) {
+ const GURL url1(L"http://www.google.com/");
+ URLRow url_info1(url1);
+ url_info1.set_title(L"Google");
+ url_info1.set_visit_count(4);
+ url_info1.set_typed_count(2);
+ url_info1.set_last_visit(Time::Now() - TimeDelta::FromDays(1));
+ url_info1.set_hidden(false);
+ URLID url_id = AddURL(url_info1);
+ ASSERT_TRUE(url_id != 0);
+
+ // Add a keyword visit.
+ ASSERT_TRUE(SetKeywordSearchTermsForURL(url_id, 1, L"visit"));
+
+ // Delete the url.
+ ASSERT_TRUE(DeleteURLRow(url_id));
+
+ // Make sure the keyword visit was deleted.
+ std::vector<KeywordSearchTermVisit> matches;
+ GetMostRecentKeywordSearchTerms(1, L"visit", 10, &matches);
+ ASSERT_EQ(0, matches.size());
+}
+
+} // namespace history \ No newline at end of file
diff --git a/chrome/browser/history/visit_database.cc b/chrome/browser/history/visit_database.cc
new file mode 100644
index 0000000..5001ec1
--- /dev/null
+++ b/chrome/browser/history/visit_database.cc
@@ -0,0 +1,392 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include <algorithm>
+#include <limits>
+#include <map>
+#include <set>
+
+#include "chrome/browser/history/visit_database.h"
+
+#include "chrome/browser/history/url_database.h"
+#include "chrome/common/page_transition_types.h"
+
+// Rows, in order, of the visit table.
+#define HISTORY_VISIT_ROW_FIELDS \
+ " id,url,visit_time,from_visit,transition,segment_id,is_indexed "
+
+namespace history {
+
+VisitDatabase::VisitDatabase() {
+}
+
+VisitDatabase::~VisitDatabase() {
+}
+
+bool VisitDatabase::InitVisitTable() {
+ if (!DoesSqliteTableExist(GetDB(), "visits")) {
+ if (sqlite3_exec(GetDB(), "CREATE TABLE visits("
+ "id INTEGER PRIMARY KEY,"
+ "url INTEGER NOT NULL," // key of the URL this corresponds to
+ "visit_time INTEGER NOT NULL,"
+ "from_visit INTEGER,"
+ "transition INTEGER DEFAULT 0 NOT NULL,"
+ "segment_id INTEGER,"
+ "is_indexed BOOLEAN)", // True when we have indexed data for this visit.
+ NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ } else if (!DoesSqliteColumnExist(GetDB(), "visits",
+ "is_indexed", "BOOLEAN")) {
+ // Old versions don't have the is_indexed column, we can just add that and
+ // not worry about different database revisions, since old ones will
+ // continue to work.
+ //
+ // TODO(brettw) this should be removed once we think everybody has been
+ // updated (added early Mar 2008).
+ if (sqlite3_exec(GetDB(),
+ "ALTER TABLE visits ADD COLUMN is_indexed BOOLEAN",
+ NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ }
+
+ // Index over url so we can quickly find visits for a page. This will just
+ // fail if it already exists and we'll ignore it.
+ sqlite3_exec(GetDB(), "CREATE INDEX visits_url_index ON visits (url)",
+ NULL, NULL, NULL);
+
+ // Create an index over from visits so that we can efficiently find
+ // referrers and redirects. Ignore failures because it likely already exists.
+ sqlite3_exec(GetDB(), "CREATE INDEX visits_from_index ON visits (from_visit)",
+ NULL, NULL, NULL);
+
+ // Create an index over time so that we can efficiently find the visits in a
+ // given time range (most history views are time-based). Ignore failures
+ // because it likely already exists.
+ sqlite3_exec(GetDB(), "CREATE INDEX visits_time_index ON visits (visit_time)",
+ NULL, NULL, NULL);
+
+ return true;
+}
+
+bool VisitDatabase::DropVisitTable() {
+ // This will also drop the indices over the table.
+ return sqlite3_exec(GetDB(), "DROP TABLE visits", NULL, NULL, NULL) ==
+ SQLITE_OK;
+}
+
+// Must be in sync with HISTORY_VISIT_ROW_FIELDS.
+// static
+void VisitDatabase::FillVisitRow(SQLStatement& statement, VisitRow* visit) {
+ visit->visit_id = statement.column_int64(0);
+ visit->url_id = statement.column_int64(1);
+ visit->visit_time = Time::FromInternalValue(statement.column_int64(2));
+ visit->referring_visit = statement.column_int64(3);
+ visit->transition = PageTransition::FromInt(statement.column_int(4));
+ visit->segment_id = statement.column_int64(5);
+ visit->is_indexed = !!statement.column_int(6);
+}
+
+// static
+void VisitDatabase::FillVisitVector(SQLStatement& statement,
+ VisitVector* visits) {
+ while (statement.step() == SQLITE_ROW) {
+ history::VisitRow visit;
+ FillVisitRow(statement, &visit);
+ visits->push_back(visit);
+ }
+}
+
+VisitID VisitDatabase::AddVisit(VisitRow* visit) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "INSERT INTO visits("
+ "url,visit_time,from_visit,transition,segment_id,is_indexed)"
+ "VALUES(?,?,?,?,?,?)");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_int64(0, visit->url_id);
+ statement->bind_int64(1, visit->visit_time.ToInternalValue());
+ statement->bind_int64(2, visit->referring_visit);
+ statement->bind_int64(3, visit->transition);
+ statement->bind_int64(4, visit->segment_id);
+ statement->bind_int64(5, visit->is_indexed);
+ if (statement->step() != SQLITE_DONE)
+ return 0;
+
+ visit->visit_id = sqlite3_last_insert_rowid(GetDB());
+ return visit->visit_id;
+}
+
+void VisitDatabase::DeleteVisit(const VisitRow& visit) {
+ // Patch around this visit. Any visits that this went to will now have their
+ // "source" be the deleted visit's source.
+ SQLITE_UNIQUE_STATEMENT(update_chain, GetStatementCache(),
+ "UPDATE visits SET from_visit=? "
+ "WHERE from_visit=?");
+ if (!update_chain.is_valid())
+ return;
+ update_chain->bind_int64(0, visit.referring_visit);
+ update_chain->bind_int64(1, visit.visit_id);
+ update_chain->step();
+
+ // Now delete the actual visit.
+ SQLITE_UNIQUE_STATEMENT(del, GetStatementCache(),
+ "DELETE FROM visits WHERE id=?");
+ if (!del.is_valid())
+ return;
+ del->bind_int64(0, visit.visit_id);
+ del->step();
+}
+
+bool VisitDatabase::GetRowForVisit(VisitID visit_id, VisitRow* out_visit) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits WHERE id=?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, visit_id);
+ if (statement->step() != SQLITE_ROW)
+ return false;
+
+ FillVisitRow(*statement, out_visit);
+ return true;
+}
+
+bool VisitDatabase::UpdateVisitRow(const VisitRow& visit) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "UPDATE visits SET "
+ "url=?,visit_time=?,from_visit=?,transition=?,segment_id=?,is_indexed=? "
+ "WHERE id=?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, visit.url_id);
+ statement->bind_int64(1, visit.visit_time.ToInternalValue());
+ statement->bind_int64(2, visit.referring_visit);
+ statement->bind_int64(3, visit.transition);
+ statement->bind_int64(4, visit.segment_id);
+ statement->bind_int64(5, visit.is_indexed);
+ statement->bind_int64(6, visit.visit_id);
+ return statement->step() == SQLITE_DONE;
+}
+
+bool VisitDatabase::GetVisitsForURL(URLID url_id, VisitVector* visits) {
+ visits->clear();
+
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_VISIT_ROW_FIELDS
+ "FROM visits "
+ "WHERE url=? "
+ "ORDER BY visit_time ASC");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, url_id);
+ FillVisitVector(*statement, visits);
+ return true;
+}
+
+void VisitDatabase::GetAllVisitsInRange(Time begin_time, Time end_time,
+ int max_results,
+ VisitVector* visits) {
+ visits->clear();
+
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
+ "WHERE visit_time >= ? AND visit_time < ?"
+ "ORDER BY visit_time LIMIT ?");
+ if (!statement.is_valid())
+ return;
+
+ // See GetVisibleVisitsInRange for more info on how these times are bound.
+ int64 end = end_time.ToInternalValue();
+ statement->bind_int64(0, begin_time.ToInternalValue());
+ statement->bind_int64(1, end ? end : std::numeric_limits<int64>::max());
+ statement->bind_int64(2,
+ max_results ? max_results : std::numeric_limits<int64>::max());
+
+ FillVisitVector(*statement, visits);
+}
+
+void VisitDatabase::GetVisibleVisitsInRange(Time begin_time, Time end_time,
+ bool most_recent_visit_only,
+ int max_count,
+ VisitVector* visits) {
+ visits->clear();
+ // The visit_time values can be duplicated in a redirect chain, so we sort
+ // by id too, to ensure a consistent ordering just in case.
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
+ "WHERE visit_time >= ? AND visit_time < ? "
+ "AND (transition & ?) != 0 " // CHAIN_END
+ "AND (transition & ?) NOT IN (?, ?) " // NO SUBFRAME
+ "ORDER BY visit_time DESC, id DESC");
+ if (!statement.is_valid())
+ return;
+
+ // Note that we use min/max values for querying unlimited ranges of time using
+ // the same statement. Since the time has an index, this will be about the
+ // same amount of work as just doing a query for everything with no qualifier.
+ int64 end = end_time.ToInternalValue();
+ statement->bind_int64(0, begin_time.ToInternalValue());
+ statement->bind_int64(1, end ? end : std::numeric_limits<int64>::max());
+ statement->bind_int(2, PageTransition::CHAIN_END);
+ statement->bind_int(3, PageTransition::CORE_MASK);
+ statement->bind_int(4, PageTransition::AUTO_SUBFRAME);
+ statement->bind_int(5, PageTransition::MANUAL_SUBFRAME);
+
+ std::set<URLID> found_urls;
+ while (statement->step() == SQLITE_ROW) {
+ VisitRow visit;
+ FillVisitRow(*statement, &visit);
+ if (most_recent_visit_only) {
+ // Make sure the URL this visit corresponds to is unique if required.
+ if (found_urls.find(visit.url_id) != found_urls.end())
+ continue;
+ found_urls.insert(visit.url_id);
+ }
+ visits->push_back(visit);
+
+ if (max_count > 0 && static_cast<int>(visits->size()) >= max_count)
+ break;
+ }
+}
+
+VisitID VisitDatabase::GetMostRecentVisitForURL(URLID url_id,
+ VisitRow* visit_row) {
+ // The visit_time values can be duplicated in a redirect chain, so we sort
+ // by id too, to ensure a consistent ordering just in case.
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_VISIT_ROW_FIELDS "FROM visits "
+ "WHERE url=? "
+ "ORDER BY visit_time DESC, id DESC "
+ "LIMIT 1");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_int64(0, url_id);
+ if (statement->step() != SQLITE_ROW)
+ return 0; // No visits for this URL.
+
+ if (visit_row) {
+ FillVisitRow(*statement, visit_row);
+ return visit_row->visit_id;
+ }
+ return statement->column_int64(0);
+}
+
+bool VisitDatabase::GetMostRecentVisitsForURL(URLID url_id,
+ int max_results,
+ VisitVector* visits) {
+ visits->clear();
+
+ // The visit_time values can be duplicated in a redirect chain, so we sort
+ // by id too, to ensure a consistent ordering just in case.
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT" HISTORY_VISIT_ROW_FIELDS
+ "FROM visits "
+ "WHERE url=? "
+ "ORDER BY visit_time DESC, id DESC "
+ "LIMIT ?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, url_id);
+ statement->bind_int(1, max_results);
+ FillVisitVector(*statement, visits);
+ return true;
+}
+
+bool VisitDatabase::GetRedirectFromVisit(VisitID from_visit,
+ VisitID* to_visit,
+ GURL* to_url) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT v.id,u.url "
+ "FROM visits v JOIN urls u ON v.url = u.id "
+ "WHERE v.from_visit = ? "
+ "AND (v.transition & ?) != 0"); // IS_REDIRECT_MASK
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, from_visit);
+ statement->bind_int(1, PageTransition::IS_REDIRECT_MASK);
+
+ if (statement->step() != SQLITE_ROW)
+ return false; // No redirect from this visit.
+ if (to_visit)
+ *to_visit = statement->column_int64(0);
+ if (to_url)
+ *to_url = GURL(statement->column_string(1));
+ return true;
+}
+
+bool VisitDatabase::GetVisitCountToHost(const GURL& url,
+ int* count,
+ Time* first_visit) {
+ if (!url.SchemeIs("http") && !url.SchemeIs("https"))
+ return false;
+
+ // We need to search for URLs with a matching host/port. One way to query for
+ // this is to use the LIKE operator, eg 'url LIKE http://google.com/%'. This
+ // is inefficient though in that it doesn't use the index and each entry must
+ // be visited. The same query can be executed by using >= and < operator.
+ // The query becomes:
+ // 'url >= http://google.com/' and url < http://google.com0'.
+ // 0 is used as it is one character greater than '/'.
+ GURL search_url(url);
+ const std::string host_query_min = search_url.GetOrigin().spec();
+
+ if (host_query_min.empty())
+ return false;
+
+ std::string host_query_max = host_query_min;
+ host_query_max[host_query_max.size() - 1] = '0';
+
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT MIN(v.visit_time), COUNT(*) "
+ "FROM visits v INNER JOIN urls u ON v.url = u.id "
+ "WHERE (u.url >= ? AND u.url < ?)");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_string(0, host_query_min);
+ statement->bind_string(1, host_query_max);
+
+ if (statement->step() != SQLITE_ROW) {
+ // We've never been to this page before.
+ *count = 0;
+ return true;
+ }
+
+ *first_visit = Time::FromInternalValue(statement->column_int64(0));
+ *count = statement->column_int(1);
+ return true;
+}
+
+} // namespace history
diff --git a/chrome/browser/history/visit_database.h b/chrome/browser/history/visit_database.h
new file mode 100644
index 0000000..cc53c0e
--- /dev/null
+++ b/chrome/browser/history/visit_database.h
@@ -0,0 +1,171 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_VISIT_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_VISIT_DATABASE_H__
+
+#include "chrome/browser/history/history_types.h"
+
+struct sqlite3;
+class SqliteStatementCache;
+class SQLStatement;
+
+namespace history {
+
+// A visit database is one which stores visits for URLs, that is, times and
+// linking information. A visit database must also be a URLDatabase, as this
+// modifies tables used by URLs directly and could be thought of as inheriting
+// from URLDatabase. However, this inheritance is not explicit as things would
+// get too complicated and have multiple inheritance.
+class VisitDatabase {
+ public:
+ // Must call InitVisitTable() before using to make sure the database is
+ // initialized.
+ VisitDatabase();
+ virtual ~VisitDatabase();
+
+ // Deletes the visit table. Used for rapidly clearing all visits. In this
+ // case, InitVisitTable would be called immediately afterward to re-create it.
+ // Returns true on success.
+ bool DropVisitTable();
+
+ // Adds a line to the visit database with the given information, returning
+ // the added row ID on success, 0 on failure. The given visit is updated with
+ // the new row ID on success.
+ VisitID AddVisit(VisitRow* visit);
+
+ // Deletes the given visit from the database. If a visit with the given ID
+ // doesn't exist, it will not do anything.
+ void DeleteVisit(const VisitRow& visit);
+
+ // Query a VisitInfo giving an visit id, filling the given VisitRow.
+ // Returns true on success.
+ bool GetRowForVisit(VisitID visit_id, VisitRow* out_visit);
+
+ // Updates an existing row. The new information is set on the row, using the
+ // VisitID as the key. The visit must exist. Returns true on success.
+ bool UpdateVisitRow(const VisitRow& visit);
+
+ // Fills in the given vector with all of the visits for the given page ID,
+ // sorted in ascending order of date. Returns true on success (although there
+ // may still be no matches).
+ bool GetVisitsForURL(URLID url_id, VisitVector* visits);
+
+ // Fills all visits in the time range [begin, end) to the given vector. Either
+ // time can be is_null(), in which case the times in that direction are
+ // unbounded.
+ //
+ // If |max_results| is non-zero, up to that many results will be returned. If
+ // there are more results than that, the oldest ones will be returned. (This
+ // is used for history expiration.)
+ //
+ // The results will be in increasing order of date.
+ void GetAllVisitsInRange(Time begin_time, Time end_time, int max_results,
+ VisitVector* visits);
+
+ // Fills all visits in the given time range into the given vector that should
+ // be user-visible, which excludes things like redirects and subframes. The
+ // begin time is inclusive, the end time is exclusive. Either time can be
+ // is_null(), in which case the times in that direction are unbounded.
+ //
+ // Up to |max_count| visits will be returned. If there are more visits than
+ // that, the most recent |max_count| will be returned. If 0, all visits in the
+ // range will be computed.
+ //
+ // When |most_recent_visit_only| is set, only one visit for each URL will be
+ // returned, and it will be the most recent one in the time range.
+ void GetVisibleVisitsInRange(Time begin_time, Time end_time,
+ bool most_recent_visit_only,
+ int max_count,
+ VisitVector* visits);
+
+ // Returns the visit ID for the most recent visit of the given URL ID, or 0
+ // if there is no visit for the URL.
+ //
+ // If non-NULL, the given visit row will be filled with the information of
+ // the found visit. When no visit is found, the row will be unchanged.
+ VisitID GetMostRecentVisitForURL(URLID url_id,
+ VisitRow* visit_row);
+
+ // Returns the |max_results| most recent visit sessions for |url_id|.
+ //
+ // Returns false if there's a failure preparing the statement. True
+ // otherwise. (No results are indicated with an empty |visits|
+ // vector.)
+ bool GetMostRecentVisitsForURL(URLID url_id,
+ int max_results,
+ VisitVector* visits);
+
+ // Finds a redirect coming from the given |from_visit|. If a redirect is
+ // found, it fills the visit ID and URL into the out variables and returns
+ // true. If there is no redirect from the given visit, returns false.
+ //
+ // If there is more than one redirect, this will compute a random one. But
+ // duplicates should be very rare, and we don't actually care which one we
+ // get in most cases. These will occur when the user goes back and gets
+ // redirected again.
+ //
+ // to_visit and to_url can be NULL in which case they are ignored.
+ bool GetRedirectFromVisit(VisitID from_visit,
+ VisitID* to_visit,
+ GURL* to_url);
+
+ // Returns the number of visits to all urls on the scheme/host/post
+ // identified by url. This is only valid for http and https urls (all other
+ // schemes are ignored and false is returned).
+ // count is set to the number of visits, first_visit is set to the first time
+ // the host was visited. Returns true on success.
+ bool GetVisitCountToHost(const GURL& url, int* count, Time* first_visit);
+
+ protected:
+ // Returns the database and statement cache for the functions in this
+ // interface. The decendent of this class implements these functions to
+ // return its objects.
+ virtual sqlite3* GetDB() = 0;
+ virtual SqliteStatementCache& GetStatementCache() = 0;
+
+ // Called by the derived classes on initialization to make sure the tables
+ // and indices are properly set up. Must be called before anything else.
+ bool InitVisitTable();
+
+ // Convenience to fill a VisitRow. Assumes the visit values are bound starting
+ // at index 0.
+ static void FillVisitRow(SQLStatement& statement, VisitRow* visit);
+
+ // Convenience to fill a VisitVector. Assumes that statement.step()
+ // hasn't happened yet.
+ static void FillVisitVector(SQLStatement& statement, VisitVector* visits);
+
+ private:
+ DISALLOW_EVIL_CONSTRUCTORS(VisitDatabase);
+};
+
+} // history
+
+#endif // CHROME_BROWSER_HISTORY_VISIT_DATABASE_H__
diff --git a/chrome/browser/history/visit_database_unittest.cc b/chrome/browser/history/visit_database_unittest.cc
new file mode 100644
index 0000000..7157ceb
--- /dev/null
+++ b/chrome/browser/history/visit_database_unittest.cc
@@ -0,0 +1,265 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "base/file_util.h"
+#include "base/path_service.h"
+#include "base/string_util.h"
+#include "chrome/browser/history/url_database.h"
+#include "chrome/browser/history/visit_database.h"
+#include "chrome/common/sqlite_compiled_statement.h"
+#include "chrome/common/sqlite_utils.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace history {
+
+namespace {
+
+bool IsVisitInfoEqual(const VisitRow& a,
+ const VisitRow& b) {
+ return a.visit_id == b.visit_id &&
+ a.url_id == b.url_id &&
+ a.visit_time == b.visit_time &&
+ a.referring_visit == b.referring_visit &&
+ a.transition == b.transition &&
+ a.is_indexed == b.is_indexed;
+}
+
+} // namespace
+
+class VisitDatabaseTest : public testing::Test,
+ public URLDatabase,
+ public VisitDatabase {
+ public:
+ VisitDatabaseTest() : db_(NULL), statement_cache_(NULL) {
+ }
+
+ private:
+ // Test setup.
+ void SetUp() {
+ PathService::Get(base::DIR_TEMP, &db_file_);
+ db_file_.push_back(file_util::kPathSeparator);
+ db_file_.append(L"VisitTest.db");
+ file_util::Delete(db_file_, false);
+
+ EXPECT_EQ(SQLITE_OK, sqlite3_open(WideToUTF8(db_file_).c_str(), &db_));
+ statement_cache_ = new SqliteStatementCache(db_);
+
+ // Initialize the tables for this test.
+ CreateURLTable(false);
+ CreateMainURLIndex();
+ CreateSupplimentaryURLIndices();
+ InitVisitTable();
+ }
+ void TearDown() {
+ delete statement_cache_;
+ sqlite3_close(db_);
+ file_util::Delete(db_file_, false);
+ }
+
+ // Provided for URL/VisitDatabase.
+ virtual sqlite3* GetDB() {
+ return db_;
+ }
+ virtual SqliteStatementCache& GetStatementCache() {
+ return *statement_cache_;
+ }
+
+ std::wstring db_file_;
+ sqlite3* db_;
+ SqliteStatementCache* statement_cache_;
+};
+
+TEST_F(VisitDatabaseTest, Add) {
+ // Add one visit.
+ VisitRow visit_info1(1, Time::Now(), 0, PageTransition::LINK, 0);
+ EXPECT_TRUE(AddVisit(&visit_info1));
+
+ // Add second visit for the same page.
+ VisitRow visit_info2(visit_info1.url_id,
+ visit_info1.visit_time + TimeDelta::FromSeconds(1), 1,
+ PageTransition::TYPED, 0);
+ EXPECT_TRUE(AddVisit(&visit_info2));
+
+ // Add third visit for a different page.
+ VisitRow visit_info3(2,
+ visit_info1.visit_time + TimeDelta::FromSeconds(2), 0,
+ PageTransition::LINK, 0);
+ EXPECT_TRUE(AddVisit(&visit_info3));
+
+ // Query the first two.
+ std::vector<VisitRow> matches;
+ EXPECT_TRUE(GetVisitsForURL(visit_info1.url_id, &matches));
+ EXPECT_EQ(2, matches.size());
+
+ // Make sure we got both (order in result set is visit time).
+ EXPECT_TRUE(IsVisitInfoEqual(matches[0], visit_info1) &&
+ IsVisitInfoEqual(matches[1], visit_info2));
+}
+
+TEST_F(VisitDatabaseTest, Delete) {
+ // Add three visits that form a chain of navigation, and then delete the
+ // middle one. We should be left with the outer two visits, and the chain
+ // should link them.
+ static const int kTime1 = 1000;
+ VisitRow visit_info1(1, Time::FromInternalValue(kTime1), 0,
+ PageTransition::LINK, 0);
+ EXPECT_TRUE(AddVisit(&visit_info1));
+
+ static const int kTime2 = kTime1 + 1;
+ VisitRow visit_info2(1, Time::FromInternalValue(kTime2),
+ visit_info1.visit_id, PageTransition::LINK, 0);
+ EXPECT_TRUE(AddVisit(&visit_info2));
+
+ static const int kTime3 = kTime2 + 1;
+ VisitRow visit_info3(1, Time::FromInternalValue(kTime3),
+ visit_info2.visit_id, PageTransition::LINK, 0);
+ EXPECT_TRUE(AddVisit(&visit_info3));
+
+ // First make sure all the visits are there.
+ std::vector<VisitRow> matches;
+ EXPECT_TRUE(GetVisitsForURL(visit_info1.url_id, &matches));
+ EXPECT_EQ(3, matches.size());
+ EXPECT_TRUE(IsVisitInfoEqual(matches[0], visit_info1) &&
+ IsVisitInfoEqual(matches[1], visit_info2) &&
+ IsVisitInfoEqual(matches[2], visit_info3));
+
+ // Delete the middle one.
+ DeleteVisit(visit_info2);
+
+ // The outer two should be left, and the last one should have the first as
+ // the referrer.
+ visit_info3.referring_visit = visit_info1.visit_id;
+ matches.clear();
+ EXPECT_TRUE(GetVisitsForURL(visit_info1.url_id, &matches));
+ EXPECT_EQ(2, matches.size());
+ EXPECT_TRUE(IsVisitInfoEqual(matches[0], visit_info1) &&
+ IsVisitInfoEqual(matches[1], visit_info3));
+}
+
+TEST_F(VisitDatabaseTest, Update) {
+ // Make something in the database.
+ VisitRow original(1, Time::Now(), 23, 22, 19);
+ AddVisit(&original);
+
+ // Mutate that row.
+ VisitRow modification(original);
+ modification.url_id = 2;
+ modification.transition = PageTransition::TYPED;
+ modification.visit_time = Time::Now() + TimeDelta::FromDays(1);
+ modification.referring_visit = 9292;
+ modification.is_indexed = true;
+ UpdateVisitRow(modification);
+
+ // Check that the mutated version was written.
+ VisitRow final;
+ GetRowForVisit(original.visit_id, &final);
+ EXPECT_TRUE(IsVisitInfoEqual(modification, final));
+}
+
+// TODO(brettw) write test for GetMostRecentVisitForURL!
+
+TEST_F(VisitDatabaseTest, GetVisibleVisitsInRange) {
+ // Add one visit.
+ VisitRow visit_info1(1, Time::Now(), 0,
+ static_cast<PageTransition::Type>(PageTransition::LINK |
+ PageTransition::CHAIN_START |
+ PageTransition::CHAIN_END),
+ 0);
+ visit_info1.visit_id = 1;
+ EXPECT_TRUE(AddVisit(&visit_info1));
+
+ // Add second visit for the same page.
+ VisitRow visit_info2(visit_info1.url_id,
+ visit_info1.visit_time + TimeDelta::FromSeconds(1), 1,
+ static_cast<PageTransition::Type>(PageTransition::TYPED |
+ PageTransition::CHAIN_START |
+ PageTransition::CHAIN_END),
+ 0);
+ visit_info2.visit_id = 2;
+ EXPECT_TRUE(AddVisit(&visit_info2));
+
+ // Add third visit for a different page.
+ VisitRow visit_info3(2,
+ visit_info1.visit_time + TimeDelta::FromSeconds(2), 0,
+ static_cast<PageTransition::Type>(PageTransition::LINK |
+ PageTransition::CHAIN_START),
+ 0);
+ visit_info3.visit_id = 3;
+ EXPECT_TRUE(AddVisit(&visit_info3));
+
+ // Add a redirect visit from the last page.
+ VisitRow visit_info4(3,
+ visit_info1.visit_time + TimeDelta::FromSeconds(3), visit_info3.visit_id,
+ static_cast<PageTransition::Type>(PageTransition::SERVER_REDIRECT |
+ PageTransition::CHAIN_END),
+ 0);
+ visit_info4.visit_id = 4;
+ EXPECT_TRUE(AddVisit(&visit_info4));
+
+ // Add a subframe visit.
+ VisitRow visit_info5(4,
+ visit_info1.visit_time + TimeDelta::FromSeconds(4), visit_info4.visit_id,
+ static_cast<PageTransition::Type>(PageTransition::AUTO_SUBFRAME |
+ PageTransition::CHAIN_START |
+ PageTransition::CHAIN_END),
+ 0);
+ visit_info5.visit_id = 5;
+ EXPECT_TRUE(AddVisit(&visit_info5));
+
+ // Query the visits for all time, we should get the first 3 in descending
+ // order, but not the redirect & subframe ones later.
+ VisitVector results;
+ GetVisibleVisitsInRange(Time(), Time(), false, 0, &results);
+ ASSERT_EQ(3, results.size());
+ EXPECT_TRUE(IsVisitInfoEqual(results[0], visit_info4) &&
+ IsVisitInfoEqual(results[1], visit_info2) &&
+ IsVisitInfoEqual(results[2], visit_info1));
+
+ // If we want only the most recent one, it should give us the same results
+ // minus the first (duplicate of the second) one.
+ GetVisibleVisitsInRange(Time(), Time(), true, 0, &results);
+ ASSERT_EQ(2, results.size());
+ EXPECT_TRUE(IsVisitInfoEqual(results[0], visit_info4) &&
+ IsVisitInfoEqual(results[1], visit_info2));
+
+ // Query a time range and make sure beginning is inclusive and ending is
+ // exclusive.
+ GetVisibleVisitsInRange(visit_info2.visit_time, visit_info4.visit_time,
+ false, 0, &results);
+ ASSERT_EQ(1, results.size());
+ EXPECT_TRUE(IsVisitInfoEqual(results[0], visit_info2));
+
+ // Query for a max count and make sure we get only that number.
+ GetVisibleVisitsInRange(Time(), Time(), false, 2, &results);
+ ASSERT_EQ(2, results.size());
+ EXPECT_TRUE(IsVisitInfoEqual(results[0], visit_info4) &&
+ IsVisitInfoEqual(results[1], visit_info2));
+}
+
+} // namespace history \ No newline at end of file
diff --git a/chrome/browser/history/visit_tracker.cc b/chrome/browser/history/visit_tracker.cc
new file mode 100644
index 0000000..ef94485
--- /dev/null
+++ b/chrome/browser/history/visit_tracker.cc
@@ -0,0 +1,131 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/visit_tracker.h"
+
+#include "base/logging.h"
+
+namespace history {
+
+// When the list gets longer than 'MaxItems', CleanupTransitionList will resize
+// the list down to 'ResizeTo' size. This is so we only do few block moves of
+// the data rather than constantly shuffle stuff around in the vector.
+static const int kMaxItemsInTransitionList = 96;
+static const int kResizeBigTransitionListTo = 64;
+COMPILE_ASSERT(kResizeBigTransitionListTo < kMaxItemsInTransitionList,
+ max_items_must_be_larger_than_resize_to);
+
+VisitTracker::VisitTracker() {
+}
+
+VisitTracker::~VisitTracker() {
+ STLDeleteContainerPairSecondPointers(hosts_.begin(), hosts_.end());
+}
+
+// This function is potentially slow because it may do up to two brute-force
+// searches of the transitions list. This transitions list is kept to a
+// relatively small number by CleanupTransitionList so it shouldn't be a big
+// deal. However, if this ends up being noticable for performance, we may want
+// to optimize lookup.
+VisitID VisitTracker::GetLastVisit(const void* host,
+ int32 page_id,
+ const GURL& referrer) {
+ if (referrer.is_empty() || !host)
+ return 0;
+
+ HostList::iterator i = hosts_.find(host);
+ if (i == hosts_.end())
+ return 0; // We don't have any entries for this host.
+ TransitionList& transitions = *i->second;
+
+ // Recall that a page ID is associated with a single session history entry.
+ // In the case of automatically loaded iframes, many visits/URLs can have the
+ // same page ID.
+ //
+ // We search backwards, starting at the current page ID, for the referring
+ // URL. This won't always be correct. For example, if a render process has
+ // the same page open in two different tabs, or even in two different frames,
+ // we can get confused about which was which. We can have the renderer
+ // report more precise referrer information in the future, but this is a
+ // hard problem and doesn't affect much in terms of real-world issues.
+ //
+ // We assume that the page IDs are increasing over time, so larger IDs than
+ // the current input ID happened in the future (this will occur if the user
+ // goes back). We can ignore future transitions because if you navigate, go
+ // back, and navigate some more, we'd like to have one node with two out
+ // edges in our visit graph.
+ for (int i = static_cast<int>(transitions.size()) - 1; i >= 0; i--) {
+ if (transitions[i].page_id <= page_id && transitions[i].url == referrer) {
+ // Found it.
+ return transitions[i].visit_id;
+ }
+ }
+
+ // We can't find the referrer.
+ return 0;
+}
+
+void VisitTracker::AddVisit(const void* host,
+ int32 page_id,
+ const GURL& url,
+ VisitID visit_id) {
+ TransitionList* transitions = hosts_[host];
+ if (!transitions) {
+ transitions = new TransitionList;
+ hosts_[host] = transitions;
+ }
+
+ Transition t;
+ t.url = url;
+ t.page_id = page_id;
+ t.visit_id = visit_id;
+ transitions->push_back(t);
+
+ CleanupTransitionList(transitions);
+}
+
+void VisitTracker::NotifyRenderProcessHostDestruction(const void* host) {
+ HostList::iterator i = hosts_.find(host);
+ if (i == hosts_.end())
+ return; // We don't have any entries for this host.
+
+ delete i->second;
+ hosts_.erase(i);
+}
+
+
+void VisitTracker::CleanupTransitionList(TransitionList* transitions) {
+ if (transitions->size() <= kMaxItemsInTransitionList)
+ return; // Nothing to do.
+
+ transitions->erase(transitions->begin(),
+ transitions->begin() + kResizeBigTransitionListTo);
+}
+
+} // namespace history
diff --git a/chrome/browser/history/visit_tracker.h b/chrome/browser/history/visit_tracker.h
new file mode 100644
index 0000000..9ef108b
--- /dev/null
+++ b/chrome/browser/history/visit_tracker.h
@@ -0,0 +1,92 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_VISIT_TRACKER_H__
+#define CHROME_BROWSER_HISTORY_VISIT_TRACKER_H__
+
+#include <list>
+#include <map>
+
+#include "base/basictypes.h"
+#include "base/time.h"
+#include "chrome/browser/history/history_types.h"
+
+namespace history {
+
+// Tracks history transitions between pages. The history backend uses this to
+// link up page transitions to form a chain of page visits, and to set the
+// transition type properly.
+//
+// This class is not thread safe.
+class VisitTracker {
+ public:
+ VisitTracker();
+ ~VisitTracker();
+
+ // Notifications -------------------------------------------------------------
+
+ void AddVisit(const void* host,
+ int32 page_id,
+ const GURL& url,
+ VisitID visit_id);
+
+ // When a RenderProcessHost is destroyed, we want to clear out our saved
+ // transitions/visit IDs for it.
+ void NotifyRenderProcessHostDestruction(const void* host);
+
+ // Querying ------------------------------------------------------------------
+
+ // Returns the visit ID for the transition given information about the visit
+ // supplied by the renderer. We will return 0 if there is no appropriate
+ // referring visit.
+ VisitID GetLastVisit(const void* host, int32 page_id, const GURL& url);
+
+ private:
+ struct Transition {
+ GURL url; // URL that the event happened to.
+ int32 page_id; // ID generated by the render process host.
+ VisitID visit_id; // Visit ID generated by history.
+ };
+ typedef std::vector<Transition> TransitionList;
+ typedef std::map<const void*, TransitionList*> HostList;
+
+ // Expires oldish items in the given transition list. This keeps the list
+ // size small by removing items that are unlikely to be needed, which is
+ // important for GetReferrer which does brute-force searches of this list.
+ void CleanupTransitionList(TransitionList* transitions);
+
+ // Maps render view hosts to lists of recent transitions.
+ HostList hosts_;
+
+ DISALLOW_EVIL_CONSTRUCTORS(VisitTracker);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_VISIT_TRACKER_H__
diff --git a/chrome/browser/history/visit_tracker_unittest.cc b/chrome/browser/history/visit_tracker_unittest.cc
new file mode 100644
index 0000000..9e694eb7
--- /dev/null
+++ b/chrome/browser/history/visit_tracker_unittest.cc
@@ -0,0 +1,163 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/visit_tracker.h"
+#include "base/basictypes.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using history::VisitTracker;
+
+namespace {
+
+struct VisitToTest {
+ // Identifies the host, we'll cast this to a pointer when querying (the
+ // tracker isn't allowed to dereference this pointer).
+ int host;
+ int32 page_id;
+
+ // Used when adding this to the tracker
+ const char* url;
+ const history::VisitID visit_id;
+
+ // Used when finding the referrer
+ const char* referrer;
+
+ // the correct referring visit ID to compare to the computed one
+ history::VisitID referring_visit_id;
+};
+
+// The tracker uses RenderProcessHost pointers for scoping but never
+// dereferences them. We use ints because it's easier. This function converts
+// between the two.
+void* MakeFakeHost(int id) {
+ void* host = 0;
+ memcpy(&host, &id, sizeof(int));
+ return host;
+}
+
+void RunTest(VisitTracker* tracker, VisitToTest* test, int test_count) {
+ for (int i = 0; i < test_count; i++) {
+ // Our host pointer is actually just an int, convert it (it will not get
+ // dereferenced).
+ void* host = MakeFakeHost(test[i].host);
+
+ // Check the referrer for this visit.
+ history::VisitID ref_visit = tracker->GetLastVisit(
+ host, test[i].page_id, GURL(test[i].referrer));
+ EXPECT_EQ(test[i].referring_visit_id, ref_visit);
+
+ // Now add this visit.
+ tracker->AddVisit(host, test[i].page_id, GURL(test[i].url),
+ test[i].visit_id);
+ }
+}
+
+} // namespace
+
+// A simple test that makes sure we transition between main pages in the
+// presence of back/forward.
+TEST(VisitTracker, SimpleTransitions) {
+ VisitToTest test_simple[] = {
+ // Started here:
+ {1, 1, "http://www.google.com/", 1, "", 0},
+ // Clicked a link:
+ {1, 2, "http://images.google.com/", 2, "http://www.google.com/", 1},
+ // Went back, then clicked a link:
+ {1, 3, "http://video.google.com/", 3, "http://www.google.com/", 1},
+ };
+
+ VisitTracker tracker;
+ RunTest(&tracker, test_simple, arraysize(test_simple));
+}
+
+// Test that referrer is properly computed when there are different frame
+// navigations happening.
+TEST(VisitTracker, Frames) {
+ VisitToTest test_frames[] = {
+ // Started here:
+ {1, 1, "http://foo.com/", 1, "", 0},
+ // Which had an auto-loaded subframe:
+ {1, 1, "http://foo.com/ad.html", 2, "http://foo.com/", 1},
+ // ...and another auto-loaded subframe:
+ {1, 1, "http://foo.com/ad2.html", 3, "http://foo.com/", 1},
+ // ...and the user navigated the first subframe to somwhere else
+ {1, 2, "http://bar.com/", 4, "http://foo.com/ad.html", 2},
+ // ...and then the second subframe somewhere else
+ {1, 3, "http://fud.com/", 5, "http://foo.com/ad2.html",3},
+ // ...and then the main frame somewhere else.
+ {1, 4, "http://www.google.com/", 6, "http://foo.com/", 1},
+ };
+
+ VisitTracker tracker;
+ RunTest(&tracker, test_frames, arraysize(test_frames));
+}
+
+// Test frame navigation to make sure that the referrer is properly computed
+// when there are multiple processes navigating the same pages.
+TEST(VisitTracker, MultiProcess) {
+ VisitToTest test_processes[] = {
+ // Process 1 and 2 start here:
+ {1, 1, "http://foo.com/", 1, "", 0},
+ {2, 1, "http://foo.com/", 2, "", 0},
+ // They have some subframes:
+ {1, 1, "http://foo.com/ad.html", 3, "http://foo.com/", 1},
+ {2, 1, "http://foo.com/ad.html", 4, "http://foo.com/", 2},
+ // Subframes are navigated:
+ {1, 2, "http://bar.com/", 5, "http://foo.com/ad.html", 3},
+ {2, 2, "http://bar.com/", 6, "http://foo.com/ad.html", 4},
+ // Main frame is navigated:
+ {1, 3, "http://www.google.com/", 7, "http://foo.com/", 1},
+ {2, 3, "http://www.google.com/", 8, "http://foo.com/", 2},
+ };
+
+ VisitTracker tracker;
+ RunTest(&tracker, test_processes, arraysize(test_processes));
+}
+
+// Test that processes get removed properly.
+TEST(VisitTracker, ProcessRemove) {
+ // Simple navigation from one process.
+ VisitToTest part1[] = {
+ {1, 1, "http://www.google.com/", 1, "", 0},
+ {1, 2, "http://images.google.com/", 2, "http://www.google.com/", 1},
+ };
+
+ VisitTracker tracker;
+ RunTest(&tracker, part1, arraysize(part1));
+
+ // Say that process has been destroyed.
+ tracker.NotifyRenderProcessHostDestruction(MakeFakeHost(1));
+
+ // Simple navigation from a new process with the same ID, it should not find
+ // a referrer.
+ VisitToTest part2[] = {
+ {1, 1, "http://images.google.com/", 2, "http://www.google.com/", 0},
+ };
+ RunTest(&tracker, part2, arraysize(part2));
+}
diff --git a/chrome/browser/history/visitsegment_database.cc b/chrome/browser/history/visitsegment_database.cc
new file mode 100644
index 0000000..423b3a5
--- /dev/null
+++ b/chrome/browser/history/visitsegment_database.cc
@@ -0,0 +1,413 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "chrome/browser/history/visitsegment_database.h"
+
+#include "base/logging.h"
+#include "base/string_util.h"
+#include "chrome/browser/history/page_usage_data.h"
+#include "chrome/common/sqlite_compiled_statement.h"
+#include "chrome/common/sqlite_utils.h"
+
+// The following tables are used to store url segment information.
+//
+// segments
+// id Primary key
+// name A unique string to represent that segment. (URL derived)
+// url_id ID of the url currently used to represent this segment.
+// pres_index index used to store a fixed presentation position.
+//
+// segment_usage
+// id Primary key
+// segment_id Corresponding segment id
+// time_slot time stamp identifying for what day this entry is about
+// visit_count Number of visit in the segment
+//
+
+namespace history {
+
+VisitSegmentDatabase::VisitSegmentDatabase() {
+}
+
+VisitSegmentDatabase::~VisitSegmentDatabase() {
+}
+
+bool VisitSegmentDatabase::InitSegmentTables() {
+ // Segments table.
+ if (!DoesSqliteTableExist(GetDB(), "segments")) {
+ if (sqlite3_exec(GetDB(), "CREATE TABLE segments ("
+ "id INTEGER PRIMARY KEY,"
+ "name VARCHAR,"
+ "url_id INTEGER NON NULL,"
+ "pres_index INTEGER DEFAULT -1 NOT NULL)",
+ NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED();
+ return false;
+ }
+
+ if (sqlite3_exec(GetDB(), "CREATE INDEX segments_name ON segments(name)",
+ NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED();
+ return false;
+ }
+ }
+
+ // This was added later, so we need to try to create it even if the table
+ // already exists.
+ sqlite3_exec(GetDB(), "CREATE INDEX segments_url_id ON segments(url_id)",
+ NULL, NULL, NULL);
+
+ // Segment usage table.
+ if (!DoesSqliteTableExist(GetDB(), "segment_usage")) {
+ if (sqlite3_exec(GetDB(), "CREATE TABLE segment_usage ("
+ "id INTEGER PRIMARY KEY,"
+ "segment_id INTEGER NOT NULL,"
+ "time_slot INTEGER NOT NULL,"
+ "visit_count INTEGER DEFAULT 0 NOT NULL)",
+ NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED();
+ return false;
+ }
+ if (sqlite3_exec(GetDB(),
+ "CREATE INDEX segment_usage_time_slot_segment_id ON "
+ "segment_usage(time_slot, segment_id)",
+ NULL, NULL, NULL) != SQLITE_OK) {
+ NOTREACHED();
+ return false;
+ }
+ }
+
+ // Added in a later version, so we always need to try to creat this index.
+ sqlite3_exec(GetDB(), "CREATE INDEX segments_usage_seg_id "
+ "ON segment_usage(segment_id)",
+ NULL, NULL, NULL);
+
+ // Presentation index table.
+ //
+ // Important note:
+ // Right now, this table is only used to store the presentation index.
+ // If you need to add more columns, keep in mind that rows are currently
+ // deleted when the presentation index is changed to -1.
+ // See SetPagePresentationIndex() in this file
+ if (!DoesSqliteTableExist(GetDB(), "presentation")) {
+ if (sqlite3_exec(GetDB(), "CREATE TABLE presentation("
+ "url_id INTEGER PRIMARY KEY,"
+ "pres_index INTEGER NOT NULL)",
+ NULL, NULL, NULL) != SQLITE_OK)
+ return false;
+ }
+ return true;
+}
+
+bool VisitSegmentDatabase::DropSegmentTables() {
+ // Dropping the tables will implicitly delete the indices.
+ return
+ sqlite3_exec(GetDB(), "DROP TABLE segments", NULL, NULL, NULL) ==
+ SQLITE_OK &&
+ sqlite3_exec(GetDB(), "DROP TABLE segment_usage", NULL, NULL, NULL) ==
+ SQLITE_OK;
+}
+
+// Note: the segment name is derived from the URL but is not a URL. It is
+// a string that can be easily recreated from various URLS. Maybe this should
+// be an MD5 to limit the length.
+//
+// static
+std::string VisitSegmentDatabase::ComputeSegmentName(const GURL& url) {
+ // TODO(brettw) this should probably use the registry controlled
+ // domains service.
+ GURL::Replacements r;
+ const char kWWWDot[] = "www.";
+ const int kWWWDotLen = arraysize(kWWWDot) - 1;
+
+ std::string host = url.host();
+ const char* host_c = host.c_str();
+ // Remove www. to avoid some dups.
+ if (static_cast<int>(host.size()) > kWWWDotLen &&
+ LowerCaseEqualsASCII(host_c, host_c + kWWWDotLen, kWWWDot)) {
+ r.SetHost(host.c_str(),
+ url_parse::Component(kWWWDotLen,
+ static_cast<int>(host.size()) - kWWWDotLen));
+ }
+ // Remove other stuff we don't want.
+ r.ClearUsername();
+ r.ClearPassword();
+ r.ClearQuery();
+ r.ClearRef();
+ r.ClearPort();
+
+ return url.ReplaceComponents(r).spec();
+}
+
+SegmentID VisitSegmentDatabase::GetSegmentNamed(
+ const std::string& segment_name) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT id FROM segments WHERE name = ?");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_string(0, segment_name);
+ if (statement->step() == SQLITE_ROW)
+ return statement->column_int64(0);
+ return 0;
+}
+
+bool VisitSegmentDatabase::UpdateSegmentRepresentationURL(SegmentID segment_id,
+ URLID url_id) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "UPDATE segments SET url_id = ? WHERE id = ?");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_int64(0, url_id);
+ statement->bind_int64(1, segment_id);
+ return statement->step() == SQLITE_DONE;
+}
+
+URLID VisitSegmentDatabase::GetSegmentRepresentationURL(SegmentID segment_id) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT url_id FROM segments WHERE id = ?");
+ if (!statement.is_valid())
+ return 0;
+
+ statement->bind_int64(0, segment_id);
+ if (statement->step() == SQLITE_ROW)
+ return statement->column_int64(0);
+ return 0;
+}
+
+SegmentID VisitSegmentDatabase::CreateSegment(URLID url_id,
+ const std::string& segment_name) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "INSERT INTO segments (name, url_id) VALUES (?,?)");
+ if (!statement.is_valid())
+ return false;
+
+ statement->bind_string(0, segment_name);
+ statement->bind_int64(1, url_id);
+ if (statement->step() == SQLITE_DONE)
+ return sqlite3_last_insert_rowid(GetDB());
+ return false;
+}
+
+bool VisitSegmentDatabase::IncreaseSegmentVisitCount(SegmentID segment_id,
+ const Time& ts,
+ int amount) {
+ Time t = ts.LocalMidnight();
+
+ SQLITE_UNIQUE_STATEMENT(select, GetStatementCache(),
+ "SELECT id, visit_count FROM segment_usage "
+ "WHERE time_slot = ? AND segment_id = ?");
+ if (!select.is_valid())
+ return false;
+
+ select->bind_int64(0, t.ToInternalValue());
+ select->bind_int64(1, segment_id);
+ if (select->step() == SQLITE_ROW) {
+ SQLITE_UNIQUE_STATEMENT(update, GetStatementCache(),
+ "UPDATE segment_usage SET visit_count = ? WHERE id = ?");
+ if (!update.is_valid())
+ return false;
+
+ update->bind_int64(0, select->column_int64(1) + static_cast<int64>(amount));
+ update->bind_int64(1, select->column_int64(0));
+ return update->step() == SQLITE_DONE;
+
+ } else {
+ SQLITE_UNIQUE_STATEMENT(insert, GetStatementCache(),
+ "INSERT INTO segment_usage "
+ "(segment_id, time_slot, visit_count) VALUES (?, ?, ?)");
+ if (!insert.is_valid())
+ return false;
+
+ insert->bind_int64(0, segment_id);
+ insert->bind_int64(1, t.ToInternalValue());
+ insert->bind_int64(2, static_cast<int64>(amount));
+ return insert->step() == SQLITE_DONE;
+ }
+}
+
+void VisitSegmentDatabase::QuerySegmentUsage(
+ const Time& from_time,
+ std::vector<PageUsageData*>* results) {
+ // This function gathers the highest-ranked segments in two queries.
+ // The first gathers scores for all segments.
+ // The second gathers segment data (url, title, etc.) for the highest-ranked
+ // segments.
+ // TODO(evanm): this disregards the "presentation index", which was what was
+ // used to lock results into position. But the rest of our code currently
+ // does as well.
+
+ // How many results we return, as promised in the header file.
+ const int kResultCount = 9;
+
+ // Gather all the segment scores:
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "SELECT segment_id, time_slot, visit_count "
+ "FROM segment_usage WHERE time_slot >= ? "
+ "ORDER BY segment_id");
+ if (!statement.is_valid()) {
+ NOTREACHED();
+ return;
+ }
+
+ Time ts = from_time.LocalMidnight();
+ statement->bind_int64(0, ts.ToInternalValue());
+
+ const Time now = Time::Now();
+ SegmentID last_segment_id = 0;
+ PageUsageData* pud = NULL;
+ float score = 0;
+ while (statement->step() == SQLITE_ROW) {
+ SegmentID segment_id = statement->column_int64(0);
+ if (segment_id != last_segment_id) {
+ if (last_segment_id != 0) {
+ pud->SetScore(score);
+ results->push_back(pud);
+ }
+
+ pud = new PageUsageData(segment_id);
+ score = 0;
+ last_segment_id = segment_id;
+ }
+
+ const Time timeslot = Time::FromInternalValue(statement->column_int64(1));
+ const int visit_count = statement->column_int(2);
+ int days_ago = (now - timeslot).InDays();
+
+ // Score for this day in isolation.
+ float day_visits_score = 1.0f + log(static_cast<float>(visit_count));
+ // Recent visits count more than historical ones, so we multiply in a boost
+ // related to how long ago this day was.
+ // This boost is a curve that smoothly goes through these values:
+ // Today gets 3x, a week ago 2x, three weeks ago 1.5x, falling off to 1x
+ // at the limit of how far we reach into the past.
+ float recency_boost = 1.0f + (2.0f * (1.0f / (1.0f + days_ago/7.0f)));
+ score += recency_boost * day_visits_score;
+ }
+
+ if (last_segment_id != 0) {
+ pud->SetScore(score);
+ results->push_back(pud);
+ }
+
+ // Limit to the top kResultCount results.
+ sort(results->begin(), results->end(), PageUsageData::Predicate);
+ if (results->size() > kResultCount)
+ results->resize(kResultCount);
+
+ // Now fetch the details about the entries we care about.
+ SQLITE_UNIQUE_STATEMENT(statement2, GetStatementCache(),
+ "SELECT urls.url, urls.title FROM urls "
+ "JOIN segments ON segments.url_id = urls.id "
+ "WHERE segments.id = ?");
+ if (!statement2.is_valid()) {
+ NOTREACHED();
+ return;
+ }
+ for (size_t i = 0; i < results->size(); ++i) {
+ PageUsageData* pud = (*results)[i];
+ statement2->bind_int64(0, pud->GetID());
+ if (statement2->step() == SQLITE_ROW) {
+ std::string url;
+ std::wstring title;
+ statement2->column_string(0, &url);
+ statement2->column_string16(1, &title);
+ pud->SetURL(GURL(url));
+ pud->SetTitle(title);
+ }
+ statement2->reset();
+ }
+}
+
+void VisitSegmentDatabase::DeleteSegmentData(const Time& older_than) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "DELETE FROM segment_usage WHERE time_slot < ?");
+ if (!statement.is_valid())
+ return;
+
+ statement->bind_int64(0, older_than.LocalMidnight().ToInternalValue());
+ if (statement->step() != SQLITE_DONE)
+ NOTREACHED();
+}
+
+void VisitSegmentDatabase::SetSegmentPresentationIndex(SegmentID segment_id,
+ int index) {
+ SQLITE_UNIQUE_STATEMENT(statement, GetStatementCache(),
+ "UPDATE segments SET pres_index = ? WHERE id = ?");
+ if (!statement.is_valid())
+ return;
+
+ statement->bind_int(0, index);
+ statement->bind_int64(1, segment_id);
+ if (statement->step() != SQLITE_DONE)
+ NOTREACHED();
+}
+
+bool VisitSegmentDatabase::DeleteSegmentForURL(URLID url_id) {
+ SQLITE_UNIQUE_STATEMENT(select, GetStatementCache(),
+ "SELECT id FROM segments WHERE url_id = ?");
+ if (!select.is_valid())
+ return false;
+
+ SQLITE_UNIQUE_STATEMENT(delete_seg, GetStatementCache(),
+ "DELETE FROM segments WHERE id = ?");
+ if (!delete_seg.is_valid())
+ return false;
+
+ SQLITE_UNIQUE_STATEMENT(delete_usage, GetStatementCache(),
+ "DELETE FROM segment_usage WHERE segment_id = ?");
+ if (!delete_usage.is_valid())
+ return false;
+
+ bool r = true;
+ select->bind_int64(0, url_id);
+ // In theory there could not be more than one segment using that URL but we
+ // loop anyway to cleanup any inconsistency.
+ while (select->step() == SQLITE_ROW) {
+ SegmentID segment_id = select->column_int64(0);
+
+ delete_usage->bind_int64(0, segment_id);
+ if (delete_usage->step() != SQLITE_DONE) {
+ NOTREACHED();
+ r = false;
+ }
+
+ delete_seg->bind_int64(0, segment_id);
+ if (delete_seg->step() != SQLITE_DONE) {
+ NOTREACHED();
+ r = false;
+ }
+ delete_usage->reset();
+ delete_seg->reset();
+ }
+ return r;
+}
+
+} // namespace history
diff --git a/chrome/browser/history/visitsegment_database.h b/chrome/browser/history/visitsegment_database.h
new file mode 100644
index 0000000..412bc70
--- /dev/null
+++ b/chrome/browser/history/visitsegment_database.h
@@ -0,0 +1,112 @@
+// Copyright 2008, Google Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CHROME_BROWSER_HISTORY_VISITSEGMENT_DATABASE_H__
+#define CHROME_BROWSER_HISTORY_VISITSEGMENT_DATABASE_H__
+
+#include "base/basictypes.h"
+#include "chrome/browser/history/history_types.h"
+
+class PageUsageData;
+struct sqlite3;
+class SqliteStatementCache;
+
+namespace history {
+
+// Tracks pages used for the most visited view.
+class VisitSegmentDatabase {
+ public:
+ // Must call InitSegmentTables before using any other part of this class.
+ VisitSegmentDatabase();
+ virtual ~VisitSegmentDatabase();
+
+ // Compute a segment name given a URL. The segment name is currently the
+ // source url spec less some information such as query strings.
+ static std::string ComputeSegmentName(const GURL& url);
+
+ // Returns the ID of the segment with the corresponding name, or 0 if there
+ // is no segment with that name.
+ SegmentID GetSegmentNamed(const std::string& segment_name);
+
+ // Update the segment identified by |out_segment_id| with the provided URL ID.
+ // The URL identifies the page that will now represent the segment. If url_id
+ // is non zero, it is assumed to be the row id of |url|.
+ bool UpdateSegmentRepresentationURL(SegmentID segment_id,
+ URLID url_id);
+
+ // Return the ID of the URL currently used to represent this segment or 0 if
+ // an error occured.
+ URLID GetSegmentRepresentationURL(SegmentID segment_id);
+
+ // Create a segment for the provided URL ID with the given name. Returns the
+ // ID of the newly created segment, or 0 on failure.
+ SegmentID CreateSegment(URLID url_id, const std::string& segment_name);
+
+ // Increase the segment visit count by the provided amount. Return true on
+ // success.
+ bool IncreaseSegmentVisitCount(SegmentID segment_id, const Time& ts,
+ int amount);
+
+ // Compute the segment usage since |from_time| using the provided aggregator.
+ // A PageUsageData is added in |result| for the nine highest-scored segments.
+ void QuerySegmentUsage(const Time& from_time,
+ std::vector<PageUsageData*>* result);
+
+ // Delete all the segment usage data which is older than the provided time
+ // stamp.
+ void DeleteSegmentData(const Time& older_than);
+
+ // Change the presentation id for the segment identified by |segment_id|
+ void SetSegmentPresentationIndex(SegmentID segment_id, int index);
+
+ // Delete the segment currently using the provided url for representation.
+ // This will also delete any associated segment usage data.
+ bool DeleteSegmentForURL(URLID url_id);
+
+ protected:
+ // Returns the database and statement cache for the functions in this
+ // interface. The decendent of this class implements these functions to
+ // return its objects.
+ virtual sqlite3* GetDB() = 0;
+ virtual SqliteStatementCache& GetStatementCache() = 0;
+
+ // Creates the tables used by this class if necessary. Returns true on
+ // success.
+ bool InitSegmentTables();
+
+ // Deletes all the segment tables, returning true on success.
+ bool DropSegmentTables();
+
+ private:
+ DISALLOW_EVIL_CONSTRUCTORS(VisitSegmentDatabase);
+};
+
+} // namespace history
+
+#endif // CHROME_BROWSER_HISTORY_VISITSEGMENT_DATABASE_H__