diff options
Diffstat (limited to 'webkit/appcache')
-rw-r--r-- | webkit/appcache/appcache.cc | 78 | ||||
-rw-r--r-- | webkit/appcache/appcache.h | 18 | ||||
-rw-r--r-- | webkit/appcache/appcache_database.cc | 879 | ||||
-rw-r--r-- | webkit/appcache/appcache_database.h | 159 | ||||
-rw-r--r-- | webkit/appcache/appcache_database_unittest.cc | 439 | ||||
-rw-r--r-- | webkit/appcache/appcache_group_unittest.cc | 5 | ||||
-rw-r--r-- | webkit/appcache/appcache_host.cc | 2 | ||||
-rw-r--r-- | webkit/appcache/appcache_host.h | 5 | ||||
-rw-r--r-- | webkit/appcache/appcache_storage_impl.cc | 856 | ||||
-rw-r--r-- | webkit/appcache/appcache_storage_impl.h | 92 | ||||
-rw-r--r-- | webkit/appcache/appcache_storage_impl_unittest.cc | 1007 | ||||
-rw-r--r-- | webkit/appcache/appcache_thread.h | 10 | ||||
-rw-r--r-- | webkit/appcache/appcache_working_set.cc | 13 | ||||
-rw-r--r-- | webkit/appcache/appcache_working_set.h | 15 | ||||
-rw-r--r-- | webkit/appcache/mock_appcache_storage.cc | 12 |
15 files changed, 3565 insertions, 25 deletions
diff --git a/webkit/appcache/appcache.cc b/webkit/appcache/appcache.cc index 38d3228..ec00cc4 100644 --- a/webkit/appcache/appcache.cc +++ b/webkit/appcache/appcache.cc @@ -79,6 +79,84 @@ void AppCache::InitializeWithManifest(Manifest* manifest) { SortByLength); } +void AppCache::InitializeWithDatabaseRecords( + const AppCacheDatabase::CacheRecord& cache_record, + const std::vector<AppCacheDatabase::EntryRecord>& entries, + const std::vector<AppCacheDatabase::FallbackNameSpaceRecord>& fallbacks, + const std::vector<AppCacheDatabase::OnlineWhiteListRecord>& whitelists) { + DCHECK(cache_id_ == cache_record.cache_id); + online_whitelist_all_ = cache_record.online_wildcard; + update_time_ = cache_record.update_time; + + for (size_t i = 0; i < entries.size(); ++i) { + const AppCacheDatabase::EntryRecord& entry = entries.at(i); + AddEntry(entry.url, AppCacheEntry(entry.flags, entry.response_id)); + } + + for (size_t i = 0; i < fallbacks.size(); ++i) { + const AppCacheDatabase::FallbackNameSpaceRecord& fallback = fallbacks.at(i); + fallback_namespaces_.push_back( + FallbackNamespace(fallback.namespace_url, fallback.fallback_entry_url)); + } + + // Sort the fallback namespaces by url string length, longest to shortest, + // since longer matches trump when matching a url to a namespace. + std::sort(fallback_namespaces_.begin(), fallback_namespaces_.end(), + SortByLength); + + if (!online_whitelist_all_) { + for (size_t i = 0; i < whitelists.size(); ++i) { + online_whitelist_namespaces_.push_back(whitelists.at(i).namespace_url); + } + } +} + +void AppCache::ToDatabaseRecords( + const AppCacheGroup* group, + AppCacheDatabase::CacheRecord* cache_record, + std::vector<AppCacheDatabase::EntryRecord>* entries, + std::vector<AppCacheDatabase::FallbackNameSpaceRecord>* fallbacks, + std::vector<AppCacheDatabase::OnlineWhiteListRecord>* whitelists) { + DCHECK(group && cache_record && entries && fallbacks && whitelists); + DCHECK(entries->empty() && fallbacks->empty() && whitelists->empty()); + + cache_record->cache_id = cache_id_; + cache_record->group_id = group->group_id(); + cache_record->online_wildcard = online_whitelist_all_; + cache_record->update_time = update_time_; + + for (EntryMap::const_iterator iter = entries_.begin(); + iter != entries_.end(); ++iter) { + entries->push_back(AppCacheDatabase::EntryRecord()); + AppCacheDatabase::EntryRecord& record = entries->back(); + record.url = iter->first; + record.cache_id = cache_id_; + record.flags = iter->second.types(); + record.response_id = iter->second.response_id(); + } + + GURL origin = group->manifest_url().GetOrigin(); + + for (size_t i = 0; i < fallback_namespaces_.size(); ++i) { + fallbacks->push_back(AppCacheDatabase::FallbackNameSpaceRecord()); + AppCacheDatabase::FallbackNameSpaceRecord& record = fallbacks->back(); + record.cache_id = cache_id_; + record.origin = origin; + record.namespace_url = fallback_namespaces_[i].first; + record.fallback_entry_url = fallback_namespaces_[i].second; + } + + if (!online_whitelist_all_) { + for (size_t i = 0; i < online_whitelist_namespaces_.size(); ++i) { + whitelists->push_back(AppCacheDatabase::OnlineWhiteListRecord()); + AppCacheDatabase::OnlineWhiteListRecord& record = whitelists->back(); + record.cache_id = cache_id_; + record.namespace_url = online_whitelist_namespaces_[i]; + } + } +} + + bool AppCache::FindResponseForRequest(const GURL& url, AppCacheEntry* found_entry, AppCacheEntry* found_fallback_entry, GURL* found_fallback_namespace, bool* found_network_namespace) { diff --git a/webkit/appcache/appcache.h b/webkit/appcache/appcache.h index 6e43a85..c4446f9 100644 --- a/webkit/appcache/appcache.h +++ b/webkit/appcache/appcache.h @@ -14,6 +14,7 @@ #include "base/time.h" #include "googleurl/src/gurl.h" #include "testing/gtest/include/gtest/gtest_prod.h" +#include "webkit/appcache/appcache_database.h" #include "webkit/appcache/appcache_entry.h" #include "webkit/appcache/manifest_parser.h" @@ -75,6 +76,22 @@ class AppCache : public base::RefCounted<AppCache> { // Do not use the manifest after this call. void InitializeWithManifest(Manifest* manifest); + // Initializes the cache with the information in the database records. + void InitializeWithDatabaseRecords( + const AppCacheDatabase::CacheRecord& cache_record, + const std::vector<AppCacheDatabase::EntryRecord>& entries, + const std::vector<AppCacheDatabase::FallbackNameSpaceRecord>& fallbacks, + const std::vector<AppCacheDatabase::OnlineWhiteListRecord>& whitelists); + + // Returns the database records to be stored in the AppCacheDatabase + // to represent this cache. + void ToDatabaseRecords( + const AppCacheGroup* group, + AppCacheDatabase::CacheRecord* cache_record, + std::vector<AppCacheDatabase::EntryRecord>* entries, + std::vector<AppCacheDatabase::FallbackNameSpaceRecord>* fallbacks, + std::vector<AppCacheDatabase::OnlineWhiteListRecord>* whitelists); + bool FindResponseForRequest(const GURL& url, AppCacheEntry* found_entry, AppCacheEntry* found_fallback_entry, GURL* found_fallback_namespace, bool* found_network_namespace); @@ -82,6 +99,7 @@ class AppCache : public base::RefCounted<AppCache> { private: friend class AppCacheGroup; friend class AppCacheHost; + friend class AppCacheStorageImplTest; friend class AppCacheUpdateJobTest; friend class base::RefCounted<AppCache>; diff --git a/webkit/appcache/appcache_database.cc b/webkit/appcache/appcache_database.cc new file mode 100644 index 0000000..256def1 --- /dev/null +++ b/webkit/appcache/appcache_database.cc @@ -0,0 +1,879 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "webkit/appcache/appcache_database.h" + +#include "app/sql/connection.h" +#include "app/sql/meta_table.h" +#include "app/sql/statement.h" +#include "app/sql/transaction.h" +#include "base/auto_reset.h" +#include "base/file_util.h" +#include "base/logging.h" +#include "webkit/appcache/appcache_entry.h" + +// Schema ------------------------------------------------------------------- +namespace { + +const int kCurrentVersion = 0; +const int kCompatibleVersion = 0; + +const char* kGroupsTable = "Groups"; +const char* kCachesTable = "Caches"; +const char* kEntriesTable = "Entries"; +const char* kFallbackNameSpacesTable = "FallbackNameSpaces"; +const char* kOnlineWhiteListsTable = "OnlineWhiteLists"; + +const struct { + const char* table_name; + const char* columns; +} kTables[] = { + { kGroupsTable, + "(group_id INTEGER PRIMARY KEY," + " origin TEXT," + " manifest_url TEXT)" }, + + { kCachesTable, + "(cache_id INTEGER PRIMARY KEY," + " group_id INTEGER," + " online_wildcard INTEGER CHECK(online_wildcard IN (0, 1))," + " update_time INTEGER)" }, + + { kEntriesTable, + "(cache_id INTEGER," + " url TEXT," + " flags INTEGER," + " response_id INTEGER)" }, + + { kFallbackNameSpacesTable, + "(cache_id INTEGER," + " origin TEXT," // intentionally not normalized + " namespace_url TEXT," + " fallback_entry_url TEXT)" }, + + { kOnlineWhiteListsTable, + "(cache_id INTEGER," + " namespace_url TEXT)" }, +}; + +const struct { + const char* index_name; + const char* table_name; + const char* columns; + bool unique; +} kIndexes[] = { + { "GroupsOriginIndex", + kGroupsTable, + "(origin)", + false }, + + { "GroupsManifestIndex", + kGroupsTable, + "(manifest_url)", + true }, + + { "CachesGroupIndex", + kCachesTable, + "(group_id)", + false }, + + { "EntriesCacheIndex", + kEntriesTable, + "(cache_id)", + false }, + + { "EntriesCacheAndUrlIndex", + kEntriesTable, + "(cache_id, url)", + true }, + + { "FallbackNameSpacesCacheIndex", + kFallbackNameSpacesTable, + "(cache_id)", + false }, + + { "FallbackNameSpacesOriginIndex", + kFallbackNameSpacesTable, + "(origin)", + false }, + + { "FallbackNameSpacesCacheAndUrlIndex", + kFallbackNameSpacesTable, + "(cache_id, namespace_url)", + true }, + + { "OnlineWhiteListCacheIndex", + kOnlineWhiteListsTable, + "(cache_id)", + false }, +}; + +const int kTableCount = ARRAYSIZE_UNSAFE(kTables); +const int kIndexCount = ARRAYSIZE_UNSAFE(kIndexes); + +} // anon namespace + + +// AppCacheDatabase ---------------------------------------------------------- +namespace appcache { + +AppCacheDatabase::AppCacheDatabase(const FilePath& path) + : db_file_path_(path), has_open_error_(false), is_recreating_(false) { +} + +AppCacheDatabase::~AppCacheDatabase() { +} + +void AppCacheDatabase::CloseConnection() { + // We can't close the connection for an in-memory database w/o + // losing all of the data, so we don't do that. + if (!db_file_path_.empty()) { + meta_table_.reset(); + db_.reset(); + } +} + +bool AppCacheDatabase::FindOriginsWithGroups(std::set<GURL>* origins) { + DCHECK(origins && origins->empty()); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT DISTINCT(origin) FROM Groups"; + + sql::Statement statement; + if (!PrepareUniqueStatement(kSql, &statement)) + return false; + + while (statement.Step()) + origins->insert(GURL(statement.ColumnString(0))); + + return statement.Succeeded(); +} + +bool AppCacheDatabase::FindLastStorageIds( + int64* last_group_id, int64* last_cache_id, int64* last_response_id) { + DCHECK(last_group_id && last_cache_id && last_response_id); + + *last_group_id = 0; + *last_cache_id = 0; + *last_response_id = 0; + + if (!LazyOpen(false)) + return false; + + sql::Statement statement; + + const char* kSql1 = "SELECT MAX(group_id) FROM Groups"; + if (!PrepareUniqueStatement(kSql1, &statement) || + !statement.Step()) { + return false; + } + int64 group_id = statement.ColumnInt64(0); + + const char* kSql2 = "SELECT MAX(cache_id) FROM Caches"; + if (!PrepareUniqueStatement(kSql2, &statement) || + !statement.Step()) { + return false; + } + int64 cache_id = statement.ColumnInt64(0); + + // TODO(michaeln): SELECT MAX(responseId) FROM Where/How ? + int64 response_id = 0; + + *last_group_id = group_id; + *last_cache_id = cache_id; + *last_response_id = response_id; + return true; +} + +bool AppCacheDatabase::FindGroup(int64 group_id, GroupRecord* record) { + DCHECK(record); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT group_id, origin, manifest_url FROM Groups WHERE group_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, group_id); + if (!statement.Step() || !statement.Succeeded()) + return false; + + ReadGroupRecord(statement, record); + DCHECK(record->group_id == group_id); + return true; +} + +bool AppCacheDatabase::FindGroupForManifestUrl( + const GURL& manifest_url, GroupRecord* record) { + DCHECK(record); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT group_id, origin, manifest_url FROM Groups" + " WHERE manifest_url = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindString(0, manifest_url.spec()); + if (!statement.Step() || !statement.Succeeded()) + return false; + + ReadGroupRecord(statement, record); + DCHECK(record->manifest_url == manifest_url); + return true; +} + +bool AppCacheDatabase::FindGroupsForOrigin( + const GURL& origin, std::vector<GroupRecord>* records) { + DCHECK(records && records->empty()); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT group_id, origin, manifest_url FROM Groups WHERE origin = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindString(0, origin.spec()); + while (statement.Step()) { + records->push_back(GroupRecord()); + ReadGroupRecord(statement, &records->back()); + DCHECK(records->back().origin == origin); + } + + return statement.Succeeded(); +} + +bool AppCacheDatabase::FindGroupForCache(int64 cache_id, GroupRecord* record) { + DCHECK(record); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT g.group_id, g.origin, g.manifest_url FROM Groups g, Caches c" + " WHERE c.cache_id = ? AND c.group_id = g.group_id"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + if (!statement.Step() || !statement.Succeeded()) + return false; + + ReadGroupRecord(statement, record); + return true; +} + +bool AppCacheDatabase::InsertGroup(const GroupRecord* record) { + if (!LazyOpen(true)) + return false; + + const char* kSql = + "INSERT INTO Groups (group_id, origin, manifest_url)" + " VALUES(?, ?, ?)"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, record->group_id); + statement.BindString(1, record->origin.spec()); + statement.BindString(2, record->manifest_url.spec()); + return statement.Run(); +} + +bool AppCacheDatabase::DeleteGroup(int64 group_id) { + if (!LazyOpen(false)) + return false; + + const char* kSql = + "DELETE FROM Groups WHERE group_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, group_id); + return statement.Run(); +} + +bool AppCacheDatabase::FindCache(int64 cache_id, CacheRecord* record) { + DCHECK(record); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT cache_id, group_id, online_wildcard, update_time" + " FROM Caches WHERE cache_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + if (!statement.Step() || !statement.Succeeded()) + return false; + + ReadCacheRecord(statement, record); + return true; +} + +bool AppCacheDatabase::FindCacheForGroup(int64 group_id, CacheRecord* record) { + DCHECK(record); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT cache_id, group_id, online_wildcard, update_time" + " FROM Caches WHERE group_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, group_id); + if (!statement.Step() || !statement.Succeeded()) + return false; + + ReadCacheRecord(statement, record); + return true; +} + +bool AppCacheDatabase::InsertCache(const CacheRecord* record) { + if (!LazyOpen(true)) + return false; + + const char* kSql = + "INSERT INTO Caches (cache_id, group_id, online_wildcard," + " update_time)" + " VALUES(?, ?, ?, ?)"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, record->cache_id); + statement.BindInt64(1, record->group_id); + statement.BindBool(2, record->online_wildcard); + + // There are no convinient methods to convert TimeTicks to or + // from microseconds directly, so we compute TimeDelta's + // as an intermediary step. + statement.BindInt64(3, + (record->update_time - base::TimeTicks()).InMicroseconds()); + + return statement.Run(); +} + +bool AppCacheDatabase::DeleteCache(int64 cache_id) { + if (!LazyOpen(false)) + return false; + + const char* kSql = + "DELETE FROM Caches WHERE cache_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + return statement.Run(); +} + +bool AppCacheDatabase::FindEntriesForCache( + int64 cache_id, std::vector<EntryRecord>* records) { + DCHECK(records && records->empty()); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT cache_id, url, flags, response_id FROM Entries" + " WHERE cache_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + while (statement.Step()) { + records->push_back(EntryRecord()); + ReadEntryRecord(statement, &records->back()); + DCHECK(records->back().cache_id == cache_id); + } + + return statement.Succeeded(); +} + +bool AppCacheDatabase::FindEntriesForUrl( + const GURL& url, std::vector<EntryRecord>* records) { + DCHECK(records && records->empty()); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT cache_id, url, flags, response_id FROM Entries" + " WHERE url = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindString(0, url.spec()); + while (statement.Step()) { + records->push_back(EntryRecord()); + ReadEntryRecord(statement, &records->back()); + DCHECK(records->back().url == url); + } + + return statement.Succeeded(); +} + + +bool AppCacheDatabase::FindEntry( + int64 cache_id, const GURL& url, EntryRecord* record) { + DCHECK(record); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT cache_id, url, flags, response_id FROM Entries" + " WHERE cache_id = ? AND url = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + statement.BindString(1, url.spec()); + if (!statement.Step() || !statement.Succeeded()) + return false; + + ReadEntryRecord(statement, record); + DCHECK(record->cache_id == cache_id); + DCHECK(record->url == url); + return true; +} + +bool AppCacheDatabase::InsertEntry(const EntryRecord* record) { + if (!LazyOpen(true)) + return false; + + const char* kSql = + "INSERT INTO Entries (cache_id, url, flags, response_id)" + " VALUES(?, ?, ?, ?)"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, record->cache_id); + statement.BindString(1, record->url.spec()); + statement.BindInt(2, record->flags); + statement.BindInt64(3, record->response_id); + return statement.Run(); +} + +bool AppCacheDatabase::InsertEntryRecords( + const std::vector<EntryRecord>& records) { + std::vector<EntryRecord>::const_iterator iter = records.begin(); + while (iter != records.end()) { + if (!InsertEntry(&(*iter))) + return false; + ++iter; + } + return true; +} + +bool AppCacheDatabase::DeleteEntriesForCache(int64 cache_id) { + if (!LazyOpen(false)) + return false; + + const char* kSql = + "DELETE FROM Entries WHERE cache_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + return statement.Run(); +} + +bool AppCacheDatabase::AddEntryFlags( + const GURL& entry_url, int64 cache_id, int additional_flags) { + if (!LazyOpen(false)) + return false; + + const char* kSql = + "UPDATE Entries SET flags = flags | ? WHERE cache_id = ? AND url = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt(0, additional_flags); + statement.BindInt64(1, cache_id); + statement.BindString(2, entry_url.spec()); + return statement.Run() && db_->GetLastChangeCount(); +} + +bool AppCacheDatabase::FindFallbackNameSpacesForOrigin( + const GURL& origin, std::vector<FallbackNameSpaceRecord>* records) { + DCHECK(records && records->empty()); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT cache_id, origin, namespace_url, fallback_entry_url" + " FROM FallbackNameSpaces WHERE origin = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindString(0, origin.spec()); + while (statement.Step()) { + records->push_back(FallbackNameSpaceRecord()); + ReadFallbackNameSpaceRecord(statement, &records->back()); + DCHECK(records->back().origin == origin); + } + return statement.Succeeded(); +} + +bool AppCacheDatabase::FindFallbackNameSpacesForCache( + int64 cache_id, std::vector<FallbackNameSpaceRecord>* records) { + DCHECK(records && records->empty()); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT cache_id, origin, namespace_url, fallback_entry_url" + " FROM FallbackNameSpaces WHERE cache_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + while (statement.Step()) { + records->push_back(FallbackNameSpaceRecord()); + ReadFallbackNameSpaceRecord(statement, &records->back()); + DCHECK(records->back().cache_id == cache_id); + } + return statement.Succeeded(); +} + +bool AppCacheDatabase::InsertFallbackNameSpace( + const FallbackNameSpaceRecord* record) { + if (!LazyOpen(true)) + return false; + + const char* kSql = + "INSERT INTO FallbackNameSpaces" + " (cache_id, origin, namespace_url, fallback_entry_url)" + " VALUES (?, ?, ?, ?)"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, record->cache_id); + statement.BindString(1, record->origin.spec()); + statement.BindString(2, record->namespace_url.spec()); + statement.BindString(3, record->fallback_entry_url.spec()); + return statement.Run(); +} + +bool AppCacheDatabase::InsertFallbackNameSpaceRecords( + const std::vector<FallbackNameSpaceRecord>& records) { + std::vector<FallbackNameSpaceRecord>::const_iterator iter = records.begin(); + while (iter != records.end()) { + if (!InsertFallbackNameSpace(&(*iter))) + return false; + ++iter; + } + return true; +} + +bool AppCacheDatabase::DeleteFallbackNameSpacesForCache(int64 cache_id) { + if (!LazyOpen(false)) + return false; + + const char* kSql = + "DELETE FROM FallbackNameSpaces WHERE cache_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + return statement.Run(); +} + +bool AppCacheDatabase::FindOnlineWhiteListForCache( + int64 cache_id, std::vector<OnlineWhiteListRecord>* records) { + DCHECK(records && records->empty()); + if (!LazyOpen(false)) + return false; + + const char* kSql = + "SELECT cache_id, namespace_url FROM OnlineWhiteLists" + " WHERE cache_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + while (statement.Step()) { + records->push_back(OnlineWhiteListRecord()); + this->ReadOnlineWhiteListRecord(statement, &records->back()); + DCHECK(records->back().cache_id == cache_id); + } + return statement.Succeeded(); +} + +bool AppCacheDatabase::InsertOnlineWhiteList( + const OnlineWhiteListRecord* record) { + if (!LazyOpen(true)) + return false; + + const char* kSql = + "INSERT INTO OnlineWhiteLists (cache_id, namespace_url) VALUES (?, ?)"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, record->cache_id); + statement.BindString(1, record->namespace_url.spec()); + return statement.Run(); +} + +bool AppCacheDatabase::InsertOnlineWhiteListRecords( + const std::vector<OnlineWhiteListRecord>& records) { + std::vector<OnlineWhiteListRecord>::const_iterator iter = records.begin(); + while (iter != records.end()) { + if (!InsertOnlineWhiteList(&(*iter))) + return false; + ++iter; + } + return true; +} + +bool AppCacheDatabase::DeleteOnlineWhiteListForCache(int64 cache_id) { + if (!LazyOpen(false)) + return false; + + const char* kSql = + "DELETE FROM OnlineWhiteLists WHERE cache_id = ?"; + + sql::Statement statement; + if (!PrepareCachedStatement(SQL_FROM_HERE, kSql, &statement)) + return false; + + statement.BindInt64(0, cache_id); + return statement.Run(); +} + +bool AppCacheDatabase::PrepareUniqueStatement( + const char* sql, sql::Statement* statement) { + DCHECK(sql && statement); + statement->Assign(db_->GetUniqueStatement(sql)); + if (!statement->is_valid()) { + NOTREACHED() << db_->GetErrorMessage(); + return false; + } + return true; +} + +bool AppCacheDatabase::PrepareCachedStatement( + const sql::StatementID& id, const char* sql, sql::Statement* statement) { + DCHECK(sql && statement); + statement->Assign(db_->GetCachedStatement(id, sql)); + if (!statement->is_valid()) { + NOTREACHED() << db_->GetErrorMessage(); + return false; + } + return true; +} + +void AppCacheDatabase::ReadGroupRecord( + const sql::Statement& statement, GroupRecord* record) { + record->group_id = statement.ColumnInt64(0); + record->origin = GURL(statement.ColumnString(1)); + record->manifest_url = GURL(statement.ColumnString(2)); +} + +void AppCacheDatabase::ReadCacheRecord( + const sql::Statement& statement, CacheRecord* record) { + record->cache_id = statement.ColumnInt64(0); + record->group_id = statement.ColumnInt64(1); + record->online_wildcard = statement.ColumnBool(2); + + // There are no convinient methods to convert TimeTicks to or + // from microseconds directly, so we compute TimeDelta's + // as an intermediary step. + record->update_time = base::TimeTicks() + + base::TimeDelta::FromMicroseconds(statement.ColumnInt64(3)); +} + +void AppCacheDatabase::ReadEntryRecord( + const sql::Statement& statement, EntryRecord* record) { + record->cache_id = statement.ColumnInt64(0); + record->url = GURL(statement.ColumnString(1)); + record->flags = statement.ColumnInt(2); + record->response_id = statement.ColumnInt64(3); +} + +void AppCacheDatabase::ReadFallbackNameSpaceRecord( + const sql::Statement& statement, FallbackNameSpaceRecord* record) { + record->cache_id = statement.ColumnInt64(0); + record->origin = GURL(statement.ColumnString(1)); + record->namespace_url = GURL(statement.ColumnString(2)); + record->fallback_entry_url = GURL(statement.ColumnString(3)); +} + +void AppCacheDatabase::ReadOnlineWhiteListRecord( + const sql::Statement& statement, OnlineWhiteListRecord* record) { + record->cache_id = statement.ColumnInt64(0); + record->namespace_url = GURL(statement.ColumnString(1)); +} + +bool AppCacheDatabase::LazyOpen(bool create_if_needed) { + if (db_.get()) + return true; + + // If we tried and failed once, don't try again in the same session + // to avoid creating an incoherent mess on disk. + if (has_open_error_) + return false; + + // Avoid creating a database at all if we can. + bool use_in_memory_db = db_file_path_.empty(); + if (!create_if_needed && + (use_in_memory_db || !file_util::PathExists(db_file_path_))) { + return false; + } + + db_.reset(new sql::Connection); + bool opened = use_in_memory_db ? db_->OpenInMemory() + : db_->Open(db_file_path_); + if (opened) + db_->Preload(); + if (!opened || !EnsureDatabaseVersion()) { + has_open_error_ = true; + meta_table_.reset(); + db_.reset(); + return false; + } + + return true; +} + +bool AppCacheDatabase::EnsureDatabaseVersion() { + bool did_create_meta_table = !sql::MetaTable::DoesTableExist(db_.get()); + + meta_table_.reset(new sql::MetaTable); + if (!meta_table_->Init(db_.get(), kCurrentVersion, kCompatibleVersion)) + return false; + + if (did_create_meta_table) + return CreateSchema(); + + if (meta_table_->GetCompatibleVersionNumber() > kCurrentVersion) { + DCHECK(!did_create_meta_table); + LOG(WARNING) << "AppCache database is too new."; + return false; + } + + if (meta_table_->GetVersionNumber() < kCurrentVersion) + return UpgradeSchema(); + +#ifndef NDEBUG + for (int i = 0; i < kTableCount; ++i) { + DCHECK(db_->DoesTableExist(kTables[i].table_name)); + } +#endif + + return true; +} + +bool AppCacheDatabase::CreateSchema() { + sql::Transaction transaction(db_.get()); + if (!transaction.Begin()) + return false; + + for (int i = 0; i < kTableCount; ++i) { + std::string sql("CREATE TABLE "); + sql += kTables[i].table_name; + sql += kTables[i].columns; + if (!db_->Execute(sql.c_str())) + return false; + } + + for (int i = 0; i < kIndexCount; ++i) { + std::string sql; + if (kIndexes[i].unique) + sql += "CREATE UNIQUE INDEX "; + else + sql += "CREATE INDEX "; + sql += kIndexes[i].index_name; + sql += " ON "; + sql += kIndexes[i].table_name; + sql += kIndexes[i].columns; + if (!db_->Execute(sql.c_str())) + return false; + } + + return transaction.Commit(); +} + +bool AppCacheDatabase::UpgradeSchema() { + DCHECK(false); // We don't have any upgrades yet since we're at version 0 + + // Upgrade logic goes here + + // If there is no upgrade path for the version on disk to the current + // version, nuke everything and start over. + return DeleteExistingAndCreateNewDatabase(); +} + +bool AppCacheDatabase::DeleteExistingAndCreateNewDatabase() { + DCHECK(!db_file_path_.empty()); + DCHECK(file_util::PathExists(db_file_path_)); + + meta_table_.reset(); + db_.reset(); + + FilePath directory = db_file_path_.DirName(); + if (!file_util::Delete(directory, true) || + !file_util::CreateDirectory(directory)) { + return false; + } + + // Make sure the steps above actually deleted things. + if (file_util::PathExists(db_file_path_)) + return false; + + // So we can't go recursive. + if (is_recreating_) + return false; + + AutoReset auto_reset(&is_recreating_, true); + return LazyOpen(true); +} + +} // namespace appcache diff --git a/webkit/appcache/appcache_database.h b/webkit/appcache/appcache_database.h new file mode 100644 index 0000000..89ff701 --- /dev/null +++ b/webkit/appcache/appcache_database.h @@ -0,0 +1,159 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef WEBKIT_APPCACHE_APPCACHE_DATABASE_H_ +#define WEBKIT_APPCACHE_APPCACHE_DATABASE_H_ + +#include <set> +#include <vector> + +#include "base/basictypes.h" +#include "base/file_path.h" +#include "base/scoped_ptr.h" +#include "base/time.h" +#include "googleurl/src/gurl.h" +#include "testing/gtest/include/gtest/gtest_prod.h" + +namespace sql { +class Connection; +class MetaTable; +class Statement; +class StatementID; +} + +namespace appcache { + +class AppCacheDatabase { + public: + struct GroupRecord { + int64 group_id; + GURL origin; + GURL manifest_url; + }; + + struct CacheRecord { + int64 cache_id; + int64 group_id; + bool online_wildcard; + base::TimeTicks update_time; + }; + + struct EntryRecord { + int64 cache_id; + GURL url; + int flags; + int64 response_id; + }; + + struct FallbackNameSpaceRecord { + int64 cache_id; + GURL origin; // intentionally not normalized + GURL namespace_url; + GURL fallback_entry_url; + }; + + struct OnlineWhiteListRecord { + int64 cache_id; + GURL namespace_url; + }; + + explicit AppCacheDatabase(const FilePath& path); + ~AppCacheDatabase(); + + void CloseConnection(); + bool FindOriginsWithGroups(std::set<GURL>* origins); + bool FindLastStorageIds( + int64* last_group_id, int64* last_cache_id, int64* last_response_id); + + bool FindGroup(int64 group_id, GroupRecord* record); + bool FindGroupForManifestUrl(const GURL& manifest_url, GroupRecord* record); + bool FindGroupsForOrigin( + const GURL& origin, std::vector<GroupRecord>* records); + bool FindGroupForCache(int64 cache_id, GroupRecord* record); + bool InsertGroup(const GroupRecord* record); + bool DeleteGroup(int64 group_id); + + bool FindCache(int64 cache_id, CacheRecord* record); + bool FindCacheForGroup(int64 group_id, CacheRecord* record); + bool InsertCache(const CacheRecord* record); + bool DeleteCache(int64 cache_id); + + bool FindEntriesForCache( + int64 cache_id, std::vector<EntryRecord>* records); + bool FindEntriesForUrl( + const GURL& url, std::vector<EntryRecord>* records); + bool FindEntry(int64 cache_id, const GURL& url, EntryRecord* record); + bool InsertEntry(const EntryRecord* record); + bool InsertEntryRecords( + const std::vector<EntryRecord>& records); + bool DeleteEntriesForCache(int64 cache_id); + bool AddEntryFlags(const GURL& entry_url, int64 cache_id, + int additional_flags); + + bool FindFallbackNameSpacesForOrigin( + const GURL& origin, std::vector<FallbackNameSpaceRecord>* records); + bool FindFallbackNameSpacesForCache( + int64 cache_id, std::vector<FallbackNameSpaceRecord>* records); + bool InsertFallbackNameSpace(const FallbackNameSpaceRecord* record); + bool InsertFallbackNameSpaceRecords( + const std::vector<FallbackNameSpaceRecord>& records); + bool DeleteFallbackNameSpacesForCache(int64 cache_id); + + bool FindOnlineWhiteListForCache( + int64 cache_id, std::vector<OnlineWhiteListRecord>* records); + bool InsertOnlineWhiteList(const OnlineWhiteListRecord* record); + bool InsertOnlineWhiteListRecords( + const std::vector<OnlineWhiteListRecord>& records); + bool DeleteOnlineWhiteListForCache(int64 cache_id); + + // So our callers can wrap operations in transactions. + sql::Connection* db_connection() { + LazyOpen(true); + return db_.get(); + } + + private: + bool PrepareUniqueStatement(const char* sql, sql::Statement* statement); + bool PrepareCachedStatement( + const sql::StatementID& id, const char* sql, sql::Statement* statement); + + // Record retrieval helpers + void ReadGroupRecord(const sql::Statement& statement, GroupRecord* record); + void ReadCacheRecord(const sql::Statement& statement, CacheRecord* record); + void ReadEntryRecord(const sql::Statement& statement, EntryRecord* record); + void ReadFallbackNameSpaceRecord( + const sql::Statement& statement, FallbackNameSpaceRecord* record); + void ReadOnlineWhiteListRecord( + const sql::Statement& statement, OnlineWhiteListRecord* record); + + // Database creation + bool LazyOpen(bool create_if_needed); + bool EnsureDatabaseVersion(); + bool CreateSchema(); + bool UpgradeSchema(); + + // Deletes the existing database file and the entire directory containing + // the database file including the disk cache in which response headers + // and bodies are stored, and then creates a new database file. + bool DeleteExistingAndCreateNewDatabase(); + + FilePath db_file_path_; + scoped_ptr<sql::Connection> db_; + scoped_ptr<sql::MetaTable> meta_table_; + bool has_open_error_; + bool is_recreating_; + + FRIEND_TEST(AppCacheDatabaseTest, CacheRecords); + FRIEND_TEST(AppCacheDatabaseTest, EntryRecords); + FRIEND_TEST(AppCacheDatabaseTest, FallbackNameSpaceRecords); + FRIEND_TEST(AppCacheDatabaseTest, GroupRecords); + FRIEND_TEST(AppCacheDatabaseTest, LazyOpen); + FRIEND_TEST(AppCacheDatabaseTest, OnlineWhiteListRecords); + FRIEND_TEST(AppCacheDatabaseTest, ReCreate); + DISALLOW_COPY_AND_ASSIGN(AppCacheDatabase); +}; + +} // namespace appcache + +#endif // WEBKIT_APPCACHE_APPCACHE_DATABASE_H_ diff --git a/webkit/appcache/appcache_database_unittest.cc b/webkit/appcache/appcache_database_unittest.cc new file mode 100644 index 0000000..62ff4ca --- /dev/null +++ b/webkit/appcache/appcache_database_unittest.cc @@ -0,0 +1,439 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "testing/gtest/include/gtest/gtest.h" + +#include "app/sql/connection.h" +#include "base/file_util.h" +#include "base/scoped_temp_dir.h" +#include "webkit/appcache/appcache_database.h" +#include "webkit/appcache/appcache_entry.h" + +namespace { + +const base::TimeTicks kZeroTimeTicks; + +class TestErrorDelegate : public sql::ErrorDelegate { + public: + virtual ~TestErrorDelegate() { } + virtual int OnError( + int error, sql::Connection* connection, sql::Statement* stmt) { + return error; + } +}; + +} // namespace + +namespace appcache { + +class AppCacheDatabaseTest {}; + +TEST(AppCacheDatabaseTest, LazyOpen) { + // Use an empty file path to use an in-memory sqlite database. + const FilePath kEmptyPath; + AppCacheDatabase db(kEmptyPath); + + EXPECT_FALSE(db.LazyOpen(false)); + EXPECT_TRUE(db.LazyOpen(true)); + + int64 group_id, cache_id, response_id; + group_id = cache_id = response_id = 0; + EXPECT_TRUE(db.FindLastStorageIds(&group_id, &cache_id, &response_id)); + EXPECT_EQ(0, group_id); + EXPECT_EQ(0, cache_id); + EXPECT_EQ(0, response_id); + + std::set<GURL> origins; + EXPECT_TRUE(db.FindOriginsWithGroups(&origins)); + EXPECT_TRUE(origins.empty()); +} + +TEST(AppCacheDatabaseTest, ReCreate) { + // Real files on disk for this test. + ScopedTempDir temp_dir; + EXPECT_TRUE(temp_dir.CreateUniqueTempDir()); + const FilePath kDbFile = temp_dir.path().AppendASCII("appcache.db"); + const FilePath kNestedDir = temp_dir.path().AppendASCII("nested"); + const FilePath kOtherFile = kNestedDir.AppendASCII("other_file"); + EXPECT_TRUE(file_util::CreateDirectory(kNestedDir)); + EXPECT_EQ(3, file_util::WriteFile(kOtherFile, "foo", 3)); + + AppCacheDatabase db(kDbFile); + EXPECT_FALSE(db.LazyOpen(false)); + EXPECT_TRUE(db.LazyOpen(true)); + + EXPECT_TRUE(file_util::PathExists(kDbFile)); + EXPECT_TRUE(file_util::DirectoryExists(kNestedDir)); + EXPECT_TRUE(file_util::PathExists(kOtherFile)); + + EXPECT_TRUE(db.DeleteExistingAndCreateNewDatabase()); + + EXPECT_TRUE(file_util::PathExists(kDbFile)); + EXPECT_FALSE(file_util::DirectoryExists(kNestedDir)); + EXPECT_FALSE(file_util::PathExists(kOtherFile)); +} + +TEST(AppCacheDatabaseTest, EntryRecords) { + const FilePath kEmptyPath; + AppCacheDatabase db(kEmptyPath); + EXPECT_TRUE(db.LazyOpen(true)); + + // Set an error delegate that will make all operations return false on error. + scoped_refptr<TestErrorDelegate> error_delegate(new TestErrorDelegate); + db.db_->set_error_delegate(error_delegate); + + AppCacheDatabase::EntryRecord entry; + + entry.cache_id = 1; + entry.url = GURL("http://blah/1"); + entry.flags = AppCacheEntry::MASTER; + entry.response_id = 1; + EXPECT_TRUE(db.InsertEntry(&entry)); + EXPECT_FALSE(db.InsertEntry(&entry)); + + entry.cache_id = 2; + entry.url = GURL("http://blah/2"); + entry.flags = AppCacheEntry::EXPLICIT; + entry.response_id = 2; + EXPECT_TRUE(db.InsertEntry(&entry)); + + entry.cache_id = 2; + entry.url = GURL("http://blah/3"); + entry.flags = AppCacheEntry::MANIFEST; + entry.response_id = 3; + EXPECT_TRUE(db.InsertEntry(&entry)); + + std::vector<AppCacheDatabase::EntryRecord> found; + + EXPECT_TRUE(db.FindEntriesForCache(1, &found)); + EXPECT_EQ(1U, found.size()); + EXPECT_EQ(1, found[0].cache_id); + EXPECT_EQ(GURL("http://blah/1"), found[0].url); + EXPECT_EQ(AppCacheEntry::MASTER, found[0].flags); + EXPECT_EQ(1, found[0].response_id); + found.clear(); + + EXPECT_TRUE(db.AddEntryFlags(GURL("http://blah/1"), 1, + AppCacheEntry::FOREIGN)); + EXPECT_TRUE(db.FindEntriesForCache(1, &found)); + EXPECT_EQ(1U, found.size()); + EXPECT_EQ(AppCacheEntry::MASTER | AppCacheEntry::FOREIGN, found[0].flags); + found.clear(); + + EXPECT_TRUE(db.FindEntriesForCache(2, &found)); + EXPECT_EQ(2U, found.size()); + EXPECT_EQ(2, found[0].cache_id); + EXPECT_EQ(GURL("http://blah/2"), found[0].url); + EXPECT_EQ(AppCacheEntry::EXPLICIT, found[0].flags); + EXPECT_EQ(2, found[0].response_id); + EXPECT_EQ(2, found[1].cache_id); + EXPECT_EQ(GURL("http://blah/3"), found[1].url); + EXPECT_EQ(AppCacheEntry::MANIFEST, found[1].flags); + EXPECT_EQ(3, found[1].response_id); + found.clear(); + + EXPECT_TRUE(db.DeleteEntriesForCache(2)); + EXPECT_TRUE(db.FindEntriesForCache(2, &found)); + EXPECT_TRUE(found.empty()); + found.clear(); + + EXPECT_TRUE(db.DeleteEntriesForCache(1)); + EXPECT_FALSE(db.AddEntryFlags(GURL("http://blah/1"), 1, + AppCacheEntry::FOREIGN)); +} + +TEST(AppCacheDatabaseTest, CacheRecords) { + const FilePath kEmptyPath; + AppCacheDatabase db(kEmptyPath); + EXPECT_TRUE(db.LazyOpen(true)); + + scoped_refptr<TestErrorDelegate> error_delegate(new TestErrorDelegate); + db.db_->set_error_delegate(error_delegate); + + const AppCacheDatabase::CacheRecord kZeroRecord = {0}; + AppCacheDatabase::CacheRecord record = kZeroRecord; + EXPECT_FALSE(db.FindCache(1, &record)); + + record.cache_id = 1; + record.group_id = 1; + record.online_wildcard = true; + record.update_time = kZeroTimeTicks; + EXPECT_TRUE(db.InsertCache(&record)); + EXPECT_FALSE(db.InsertCache(&record)); + + record = kZeroRecord; + EXPECT_TRUE(db.FindCache(1, &record)); + EXPECT_EQ(1, record.cache_id); + EXPECT_EQ(1, record.group_id); + EXPECT_TRUE(record.online_wildcard); + EXPECT_TRUE(kZeroTimeTicks == record.update_time); + + record = kZeroRecord; + EXPECT_TRUE(db.FindCacheForGroup(1, &record)); + EXPECT_EQ(1, record.cache_id); + EXPECT_EQ(1, record.group_id); + EXPECT_TRUE(record.online_wildcard); + EXPECT_TRUE(kZeroTimeTicks == record.update_time); + + EXPECT_TRUE(db.DeleteCache(1)); + EXPECT_FALSE(db.FindCache(1, &record)); + EXPECT_FALSE(db.FindCacheForGroup(1, &record)); + + EXPECT_TRUE(db.DeleteCache(1)); +} + +TEST(AppCacheDatabaseTest, GroupRecords) { + const FilePath kEmptyPath; + AppCacheDatabase db(kEmptyPath); + EXPECT_TRUE(db.LazyOpen(true)); + + scoped_refptr<TestErrorDelegate> error_delegate(new TestErrorDelegate); + db.db_->set_error_delegate(error_delegate); + + const GURL kManifestUrl("http://blah/manifest"); + const GURL kOrigin(kManifestUrl.GetOrigin()); + + const AppCacheDatabase::GroupRecord kZeroRecord = {0, GURL(), GURL()}; + AppCacheDatabase::GroupRecord record = kZeroRecord; + std::vector<AppCacheDatabase::GroupRecord> records; + + // Behavior with an empty table + EXPECT_FALSE(db.FindGroup(1, &record)); + EXPECT_FALSE(db.FindGroupForManifestUrl(kManifestUrl, &record)); + EXPECT_TRUE(db.DeleteGroup(1)); + EXPECT_TRUE(db.FindGroupsForOrigin(kOrigin, &records)); + EXPECT_TRUE(records.empty()); + EXPECT_FALSE(db.FindGroupForCache(1, &record)); + + record.group_id = 1; + record.manifest_url = kManifestUrl; + record.origin = kOrigin; + EXPECT_TRUE(db.InsertGroup(&record)); + EXPECT_FALSE(db.InsertGroup(&record)); + + record.group_id = 2; + EXPECT_FALSE(db.InsertGroup(&record)); + + record = kZeroRecord; + EXPECT_TRUE(db.FindGroup(1, &record)); + EXPECT_EQ(1, record.group_id); + EXPECT_EQ(kManifestUrl, record.manifest_url); + EXPECT_EQ(kOrigin, record.origin); + + record = kZeroRecord; + EXPECT_TRUE(db.FindGroupForManifestUrl(kManifestUrl, &record)); + EXPECT_EQ(1, record.group_id); + EXPECT_EQ(kManifestUrl, record.manifest_url); + EXPECT_EQ(kOrigin, record.origin); + + record.group_id = 2; + record.manifest_url = kOrigin; + record.origin = kOrigin; + EXPECT_TRUE(db.InsertGroup(&record)); + + record = kZeroRecord; + EXPECT_TRUE(db.FindGroupForManifestUrl(kOrigin, &record)); + EXPECT_EQ(2, record.group_id); + EXPECT_EQ(kOrigin, record.manifest_url); + EXPECT_EQ(kOrigin, record.origin); + + EXPECT_TRUE(db.FindGroupsForOrigin(kOrigin, &records)); + EXPECT_EQ(2U, records.size()); + EXPECT_EQ(1, records[0].group_id); + EXPECT_EQ(kManifestUrl, records[0].manifest_url); + EXPECT_EQ(kOrigin, records[0].origin); + EXPECT_EQ(2, records[1].group_id); + EXPECT_EQ(kOrigin, records[1].manifest_url); + EXPECT_EQ(kOrigin, records[1].origin); + + EXPECT_TRUE(db.DeleteGroup(1)); + + records.clear(); + EXPECT_TRUE(db.FindGroupsForOrigin(kOrigin, &records)); + EXPECT_EQ(1U, records.size()); + EXPECT_EQ(2, records[0].group_id); + EXPECT_EQ(kOrigin, records[0].manifest_url); + EXPECT_EQ(kOrigin, records[0].origin); + + std::set<GURL> origins; + EXPECT_TRUE(db.FindOriginsWithGroups(&origins)); + EXPECT_EQ(1U, origins.size()); + EXPECT_EQ(kOrigin, *(origins.begin())); + + const GURL kManifest2("http://blah2/manifest"); + const GURL kOrigin2(kManifest2.GetOrigin()); + record.group_id = 1; + record.manifest_url = kManifest2; + record.origin = kOrigin2; + EXPECT_TRUE(db.InsertGroup(&record)); + + origins.clear(); + EXPECT_TRUE(db.FindOriginsWithGroups(&origins)); + EXPECT_EQ(2U, origins.size()); + EXPECT_TRUE(origins.end() != origins.find(kOrigin)); + EXPECT_TRUE(origins.end() != origins.find(kOrigin2)); + + AppCacheDatabase::CacheRecord cache_record; + cache_record.cache_id = 1; + cache_record.group_id = 1; + cache_record.online_wildcard = true; + cache_record.update_time = kZeroTimeTicks; + EXPECT_TRUE(db.InsertCache(&cache_record)); + + record = kZeroRecord; + EXPECT_TRUE(db.FindGroupForCache(1, &record)); + EXPECT_EQ(1, record.group_id); + EXPECT_EQ(kManifest2, record.manifest_url); + EXPECT_EQ(kOrigin2, record.origin); +} + +TEST(AppCacheDatabaseTest, FallbackNameSpaceRecords) { + const FilePath kEmptyPath; + AppCacheDatabase db(kEmptyPath); + EXPECT_TRUE(db.LazyOpen(true)); + + scoped_refptr<TestErrorDelegate> error_delegate(new TestErrorDelegate); + db.db_->set_error_delegate(error_delegate); + + const GURL kFooNameSpace1("http://foo/namespace1"); + const GURL kFooNameSpace2("http://foo/namespace2"); + const GURL kFooFallbackEntry("http://foo/entry"); + const GURL kFooOrigin(kFooNameSpace1.GetOrigin()); + const GURL kBarNameSpace1("http://bar/namespace1"); + const GURL kBarNameSpace2("http://bar/namespace2"); + const GURL kBarFallbackEntry("http://bar/entry"); + const GURL kBarOrigin(kBarNameSpace1.GetOrigin()); + + const AppCacheDatabase::FallbackNameSpaceRecord kZeroRecord = + { 0, GURL(), GURL(), GURL() }; + AppCacheDatabase::FallbackNameSpaceRecord record = kZeroRecord; + std::vector<AppCacheDatabase::FallbackNameSpaceRecord> records; + + // Behavior with an empty table + EXPECT_TRUE(db.FindFallbackNameSpacesForCache(1, &records)); + EXPECT_TRUE(records.empty()); + EXPECT_TRUE(db.FindFallbackNameSpacesForOrigin(kFooOrigin, &records)); + EXPECT_TRUE(records.empty()); + EXPECT_TRUE(db.DeleteFallbackNameSpacesForCache(1)); + + // Two records for two differenent caches in the Foo origin. + record.cache_id = 1; + record.origin = kFooOrigin; + record.namespace_url = kFooNameSpace1; + record.fallback_entry_url = kFooFallbackEntry; + EXPECT_TRUE(db.InsertFallbackNameSpace(&record)); + EXPECT_FALSE(db.InsertFallbackNameSpace(&record)); + + record.cache_id = 2; + record.origin = kFooOrigin; + record.namespace_url = kFooNameSpace2; + record.fallback_entry_url = kFooFallbackEntry; + EXPECT_TRUE(db.InsertFallbackNameSpace(&record)); + + records.clear(); + EXPECT_TRUE(db.FindFallbackNameSpacesForCache(1, &records)); + EXPECT_EQ(1U, records.size()); + EXPECT_EQ(1, records[0].cache_id); + EXPECT_EQ(kFooOrigin, records[0].origin); + EXPECT_EQ(kFooNameSpace1, records[0].namespace_url); + EXPECT_EQ(kFooFallbackEntry, records[0].fallback_entry_url); + + records.clear(); + EXPECT_TRUE(db.FindFallbackNameSpacesForCache(2, &records)); + EXPECT_EQ(1U, records.size()); + EXPECT_EQ(2, records[0].cache_id); + EXPECT_EQ(kFooOrigin, records[0].origin); + EXPECT_EQ(kFooNameSpace2, records[0].namespace_url); + EXPECT_EQ(kFooFallbackEntry, records[0].fallback_entry_url); + + records.clear(); + EXPECT_TRUE(db.FindFallbackNameSpacesForOrigin(kFooOrigin, &records)); + EXPECT_EQ(2U, records.size()); + EXPECT_EQ(1, records[0].cache_id); + EXPECT_EQ(kFooOrigin, records[0].origin); + EXPECT_EQ(kFooNameSpace1, records[0].namespace_url); + EXPECT_EQ(kFooFallbackEntry, records[0].fallback_entry_url); + EXPECT_EQ(2, records[1].cache_id); + EXPECT_EQ(kFooOrigin, records[1].origin); + EXPECT_EQ(kFooNameSpace2, records[1].namespace_url); + EXPECT_EQ(kFooFallbackEntry, records[1].fallback_entry_url); + + EXPECT_TRUE(db.DeleteFallbackNameSpacesForCache(1)); + records.clear(); + EXPECT_TRUE(db.FindFallbackNameSpacesForOrigin(kFooOrigin, &records)); + EXPECT_EQ(1U, records.size()); + EXPECT_EQ(2, records[0].cache_id); + EXPECT_EQ(kFooOrigin, records[0].origin); + EXPECT_EQ(kFooNameSpace2, records[0].namespace_url); + EXPECT_EQ(kFooFallbackEntry, records[0].fallback_entry_url); + + // Two more records for the same cache in the Bar origin. + record.cache_id = 3; + record.origin = kBarOrigin; + record.namespace_url = kBarNameSpace1; + record.fallback_entry_url = kBarFallbackEntry; + EXPECT_TRUE(db.InsertFallbackNameSpace(&record)); + + record.cache_id = 3; + record.origin = kBarOrigin; + record.namespace_url = kBarNameSpace2; + record.fallback_entry_url = kBarFallbackEntry; + EXPECT_TRUE(db.InsertFallbackNameSpace(&record)); + + records.clear(); + EXPECT_TRUE(db.FindFallbackNameSpacesForCache(3, &records)); + EXPECT_EQ(2U, records.size()); + records.clear(); + EXPECT_TRUE(db.FindFallbackNameSpacesForOrigin(kBarOrigin, &records)); + EXPECT_EQ(2U, records.size()); +} + +TEST(AppCacheDatabaseTest, OnlineWhiteListRecords) { + const FilePath kEmptyPath; + AppCacheDatabase db(kEmptyPath); + EXPECT_TRUE(db.LazyOpen(true)); + + scoped_refptr<TestErrorDelegate> error_delegate(new TestErrorDelegate); + db.db_->set_error_delegate(error_delegate); + + const GURL kFooNameSpace1("http://foo/namespace1"); + const GURL kFooNameSpace2("http://foo/namespace2"); + const GURL kBarNameSpace1("http://bar/namespace1"); + + const AppCacheDatabase::OnlineWhiteListRecord kZeroRecord = { 0, GURL() }; + AppCacheDatabase::OnlineWhiteListRecord record = kZeroRecord; + std::vector<AppCacheDatabase::OnlineWhiteListRecord> records; + + // Behavior with an empty table + EXPECT_TRUE(db.FindOnlineWhiteListForCache(1, &records)); + EXPECT_TRUE(records.empty()); + EXPECT_TRUE(db.DeleteOnlineWhiteListForCache(1)); + + record.cache_id = 1; + record.namespace_url = kFooNameSpace1; + EXPECT_TRUE(db.InsertOnlineWhiteList(&record)); + record.namespace_url = kFooNameSpace2; + EXPECT_TRUE(db.InsertOnlineWhiteList(&record)); + records.clear(); + EXPECT_TRUE(db.FindOnlineWhiteListForCache(1, &records)); + EXPECT_EQ(2U, records.size()); + EXPECT_EQ(1, records[0].cache_id); + EXPECT_EQ(kFooNameSpace1, records[0].namespace_url); + EXPECT_EQ(1, records[1].cache_id); + EXPECT_EQ(kFooNameSpace2, records[1].namespace_url); + + record.cache_id = 2; + record.namespace_url = kBarNameSpace1; + EXPECT_TRUE(db.InsertOnlineWhiteList(&record)); + records.clear(); + EXPECT_TRUE(db.FindOnlineWhiteListForCache(2, &records)); + EXPECT_EQ(1U, records.size()); + + EXPECT_TRUE(db.DeleteOnlineWhiteListForCache(1)); + records.clear(); + EXPECT_TRUE(db.FindOnlineWhiteListForCache(1, &records)); + EXPECT_TRUE(records.empty()); +} + +} // namespace appcache diff --git a/webkit/appcache/appcache_group_unittest.cc b/webkit/appcache/appcache_group_unittest.cc index 5986021..180cc3a 100644 --- a/webkit/appcache/appcache_group_unittest.cc +++ b/webkit/appcache/appcache_group_unittest.cc @@ -77,7 +77,7 @@ class AppCacheGroupTest : public testing::Test { TEST(AppCacheGroupTest, AddRemoveCache) { MockAppCacheService service; scoped_refptr<AppCacheGroup> group = - new AppCacheGroup(&service, GURL::EmptyGURL(), 111); + new AppCacheGroup(&service, GURL("http://foo.com"), 111); base::TimeTicks ticks = base::TimeTicks::Now(); @@ -146,7 +146,8 @@ TEST(AppCacheGroupTest, AddRemoveCache) { TEST(AppCacheGroupTest, CleanupUnusedGroup) { MockAppCacheService service; TestAppCacheFrontend frontend; - AppCacheGroup* group = new AppCacheGroup(&service, GURL::EmptyGURL(), 111); + AppCacheGroup* group = + new AppCacheGroup(&service, GURL("http://foo.com"), 111); AppCacheHost host1(1, &frontend, &service); AppCacheHost host2(2, &frontend, &service); diff --git a/webkit/appcache/appcache_host.cc b/webkit/appcache/appcache_host.cc index 6a62a44..489d062 100644 --- a/webkit/appcache/appcache_host.cc +++ b/webkit/appcache/appcache_host.cc @@ -300,6 +300,7 @@ void AppCacheHost::FinishCacheSelection( void AppCacheHost::ObserveGroupBeingUpdated(AppCacheGroup* group) { DCHECK(!group_being_updated_); group_being_updated_ = group; + newest_cache_of_group_being_updated_ = group->newest_complete_cache(); group->AddUpdateObserver(this); } @@ -311,6 +312,7 @@ void AppCacheHost::OnUpdateComplete(AppCacheGroup* group) { SetSwappableCache(group); group_being_updated_ = NULL; + newest_cache_of_group_being_updated_ = NULL; } void AppCacheHost::SetSwappableCache(AppCacheGroup* group) { diff --git a/webkit/appcache/appcache_host.h b/webkit/appcache/appcache_host.h index 13acdb0..3029cd3 100644 --- a/webkit/appcache/appcache_host.h +++ b/webkit/appcache/appcache_host.h @@ -135,6 +135,11 @@ class AppCacheHost : public AppCacheStorage::Delegate, // Keep a reference to the group being updated until the update completes. scoped_refptr<AppCacheGroup> group_being_updated_; + // Similarly, keep a reference to the newest cache of the group until the + // update completes. When adding a new master entry to a cache that is not + // in use in any other host, this reference keeps the cache in memory. + scoped_refptr<AppCache> newest_cache_of_group_being_updated_; + // Keep a reference to the cache of the main resource so it survives frame // navigations. scoped_refptr<AppCache> main_resource_cache_; diff --git a/webkit/appcache/appcache_storage_impl.cc b/webkit/appcache/appcache_storage_impl.cc index 3fb8280..04809eb 100644 --- a/webkit/appcache/appcache_storage_impl.cc +++ b/webkit/appcache/appcache_storage_impl.cc @@ -4,12 +4,862 @@ #include "webkit/appcache/appcache_storage_impl.h" +#include "app/sql/connection.h" +#include "app/sql/transaction.h" +#include "base/logging.h" +#include "base/message_loop.h" +#include "base/stl_util-inl.h" +#include "base/string_util.h" +#include "webkit/appcache/appcache.h" +#include "webkit/appcache/appcache_database.h" +#include "webkit/appcache/appcache_entry.h" +#include "webkit/appcache/appcache_group.h" +#include "webkit/appcache/appcache_response.h" +#include "webkit/appcache/appcache_thread.h" + namespace appcache { +static const char kAppCacheDatabaseName[] = "Index"; +static const char kDiskCacheDirectoryName[] = "Cache"; +static const int kMaxDiskCacheSize = 10 * 1024 * 1024; + +// DatabaseTask ----------------------------------------- + +class AppCacheStorageImpl::DatabaseTask + : public base::RefCountedThreadSafe<DatabaseTask> { + public: + explicit DatabaseTask(AppCacheStorageImpl* storage) + : storage_(storage), database_(storage->database_) {} + + void AddDelegate(DelegateReference* delegate_reference) { + delegates_.push_back(delegate_reference); + } + + // Schedules a task to be Run() on the DB thread. Tasks + // are run in the order in which they are scheduled. + void Schedule(); + + // Called on the DB thread. + virtual void Run() = 0; + + // Called on the IO thread after Run() has completed. + virtual void RunCompleted() {} + + // Once scheduled a task cannot be cancelled, but the + // call to RunCompleted may be. This method should only be + // called on the IO thread. This is used by AppCacheStorageImpl + // to cancel the completion calls when AppCacheStorageImpl is + // destructed. This method may be overriden to release or delete + // additional data associated with the task that is not DB thread + // safe. If overriden, this base class method must be called from + // within the override. + virtual void CancelCompletion(); + + protected: + AppCacheStorageImpl* storage_; + AppCacheDatabase* database_; + DelegateReferenceVector delegates_; + + private: + void CallRun(); + void CallRunCompleted(); +}; + +void AppCacheStorageImpl::DatabaseTask::Schedule() { + DCHECK(storage_); + DCHECK(AppCacheThread::CurrentlyOn(AppCacheThread::io())); + storage_->scheduled_database_tasks_.push_back(this); + AppCacheThread::PostTask(AppCacheThread::db(), FROM_HERE, + NewRunnableMethod(this, &DatabaseTask::CallRun)); +} + +void AppCacheStorageImpl::DatabaseTask::CancelCompletion() { + DCHECK(AppCacheThread::CurrentlyOn(AppCacheThread::io())); + delegates_.clear(); + storage_ = NULL; +} + +void AppCacheStorageImpl::DatabaseTask::CallRun() { + DCHECK(AppCacheThread::CurrentlyOn(AppCacheThread::db())); + Run(); + AppCacheThread::PostTask(AppCacheThread::io(), FROM_HERE, + NewRunnableMethod(this, &DatabaseTask::CallRunCompleted)); +} + +void AppCacheStorageImpl::DatabaseTask::CallRunCompleted() { + if (storage_) { + DCHECK(AppCacheThread::CurrentlyOn(AppCacheThread::io())); + DCHECK(storage_->scheduled_database_tasks_.front() == this); + storage_->scheduled_database_tasks_.pop_front(); + RunCompleted(); + } +} + +// InitTask ------- + +class AppCacheStorageImpl::InitTask : public DatabaseTask { + public: + explicit InitTask(AppCacheStorageImpl* storage) + : DatabaseTask(storage), last_group_id_(0), + last_cache_id_(0), last_response_id_(0) {} + + virtual void Run(); + virtual void RunCompleted(); + + int64 last_group_id_; + int64 last_cache_id_; + int64 last_response_id_; + std::set<GURL> origins_with_groups_; +}; + +void AppCacheStorageImpl::InitTask::Run() { + database_->FindLastStorageIds( + &last_group_id_, &last_cache_id_, &last_response_id_); + database_->FindOriginsWithGroups(&origins_with_groups_); +} + +void AppCacheStorageImpl::InitTask::RunCompleted() { + storage_->last_group_id_ = last_group_id_; + storage_->last_cache_id_ = last_cache_id_; + storage_->last_response_id_ = last_response_id_; + storage_->origins_with_groups_.swap(origins_with_groups_); +} + +// CloseConnectionTask ------- + +class AppCacheStorageImpl::CloseConnectionTask : public DatabaseTask { + public: + explicit CloseConnectionTask(AppCacheStorageImpl* storage) + : DatabaseTask(storage) {} + + virtual void Run() { database_->CloseConnection(); } +}; + +// StoreOrLoadTask ------- + +class AppCacheStorageImpl::StoreOrLoadTask : public DatabaseTask { + protected: + explicit StoreOrLoadTask(AppCacheStorageImpl* storage) + : DatabaseTask(storage) {} + + bool FindRelatedCacheRecords(int64 cache_id); + void CreateCacheAndGroupFromRecords( + scoped_refptr<AppCache>* cache, scoped_refptr<AppCacheGroup>* group); + + AppCacheDatabase::GroupRecord group_record_; + AppCacheDatabase::CacheRecord cache_record_; + std::vector<AppCacheDatabase::EntryRecord> entry_records_; + std::vector<AppCacheDatabase::FallbackNameSpaceRecord> + fallback_namespace_records_; + std::vector<AppCacheDatabase::OnlineWhiteListRecord> + online_whitelist_records_; +}; + +bool AppCacheStorageImpl::StoreOrLoadTask::FindRelatedCacheRecords( + int64 cache_id) { + return database_->FindEntriesForCache(cache_id, &entry_records_) && + database_->FindFallbackNameSpacesForCache( + cache_id, &fallback_namespace_records_) && + database_->FindOnlineWhiteListForCache( + cache_id, &online_whitelist_records_); +} + +void AppCacheStorageImpl::StoreOrLoadTask::CreateCacheAndGroupFromRecords( + scoped_refptr<AppCache>* cache, scoped_refptr<AppCacheGroup>* group) { + DCHECK(storage_ && cache && group); + + (*cache) = new AppCache(storage_->service_, cache_record_.cache_id); + cache->get()->InitializeWithDatabaseRecords( + cache_record_, entry_records_, fallback_namespace_records_, + online_whitelist_records_); + cache->get()->set_complete(true); + + (*group) = storage_->working_set_.GetGroup(group_record_.manifest_url); + if (group->get()) { + DCHECK(group_record_.group_id == group->get()->group_id()); + group->get()->AddCache(cache->get()); + } else { + (*group) = new AppCacheGroup( + storage_->service_, group_record_.manifest_url, + group_record_.group_id); + group->get()->AddCache(cache->get()); + } + DCHECK(group->get()->newest_complete_cache() == cache->get()); + + // We have to update foriegn entries if MarkEntryAsForeignTasks + // are in flight. + std::vector<GURL> urls; + storage_->GetPendingForeignMarkingsForCache(cache->get()->cache_id(), &urls); + for (std::vector<GURL>::iterator iter = urls.begin(); + iter != urls.end(); ++iter) { + DCHECK(cache->get()->GetEntry(*iter)); + cache->get()->GetEntry(*iter)->add_types(AppCacheEntry::FOREIGN); + } +} + +// CacheLoadTask ------- + +class AppCacheStorageImpl::CacheLoadTask : public StoreOrLoadTask { + public: + CacheLoadTask(int64 cache_id, AppCacheStorageImpl* storage) + : StoreOrLoadTask(storage), cache_id_(cache_id), + success_(false) {} + + virtual void Run(); + virtual void RunCompleted(); + + int64 cache_id_; + bool success_; +}; + +void AppCacheStorageImpl::CacheLoadTask::Run() { + success_ = + database_->FindCache(cache_id_, &cache_record_) && + database_->FindGroup(cache_record_.group_id, &group_record_) && + FindRelatedCacheRecords(cache_id_); +} + +void AppCacheStorageImpl::CacheLoadTask::RunCompleted() { + storage_->pending_cache_loads_.erase(cache_id_); + scoped_refptr<AppCache> cache; + scoped_refptr<AppCacheGroup> group; + if (success_) { + DCHECK(cache_record_.cache_id == cache_id_); + DCHECK(!storage_->working_set_.GetCache(cache_record_.cache_id)); + CreateCacheAndGroupFromRecords(&cache, &group); + } + FOR_EACH_DELEGATE(delegates_, OnCacheLoaded(cache, cache_id_)); +} + +// GroupLoadTask ------- + +class AppCacheStorageImpl::GroupLoadTask : public StoreOrLoadTask { + public: + GroupLoadTask(GURL manifest_url, AppCacheStorageImpl* storage) + : StoreOrLoadTask(storage), manifest_url_(manifest_url), + success_(false) {} + + virtual void Run(); + virtual void RunCompleted(); + + GURL manifest_url_; + bool success_; +}; + +void AppCacheStorageImpl::GroupLoadTask::Run() { + success_ = + database_->FindGroupForManifestUrl(manifest_url_, &group_record_) && + database_->FindCacheForGroup(group_record_.group_id, &cache_record_) && + FindRelatedCacheRecords(cache_record_.cache_id); +} + +void AppCacheStorageImpl::GroupLoadTask::RunCompleted() { + storage_->pending_group_loads_.erase(manifest_url_); + scoped_refptr<AppCacheGroup> group; + scoped_refptr<AppCache> cache; + if (success_) { + DCHECK(group_record_.manifest_url == manifest_url_); + DCHECK(!storage_->working_set_.GetGroup(manifest_url_)); + DCHECK(!storage_->working_set_.GetCache(cache_record_.cache_id)); + CreateCacheAndGroupFromRecords(&cache, &group); + } else { + group = new AppCacheGroup( + storage_->service_, manifest_url_, + storage_->NewGroupId()); + } + FOR_EACH_DELEGATE(delegates_, OnGroupLoaded(group, manifest_url_)); +} + +// StoreGroupAndCacheTask ------- + +class AppCacheStorageImpl::StoreGroupAndCacheTask : public StoreOrLoadTask { + public: + StoreGroupAndCacheTask(AppCacheStorageImpl* storage, AppCacheGroup* group, + AppCache* newest_cache); + + virtual void Run(); + virtual void RunCompleted(); + virtual void CancelCompletion(); + + scoped_refptr<AppCacheGroup> group_; + scoped_refptr<AppCache> cache_; + bool success_; +}; + +AppCacheStorageImpl::StoreGroupAndCacheTask::StoreGroupAndCacheTask( + AppCacheStorageImpl* storage, AppCacheGroup* group, AppCache* newest_cache) + : StoreOrLoadTask(storage), group_(group), cache_(newest_cache), + success_(false) { + group_record_.group_id = group->group_id(); + group_record_.manifest_url = group->manifest_url(); + group_record_.origin = group_record_.manifest_url.GetOrigin(); + newest_cache->ToDatabaseRecords( + group, + &cache_record_, &entry_records_, &fallback_namespace_records_, + &online_whitelist_records_); +} + +void AppCacheStorageImpl::StoreGroupAndCacheTask::Run() { + DCHECK(!success_); + sql::Connection* connection = database_->db_connection(); + if (!connection) + return; + + sql::Transaction transaction(connection); + if (!transaction.Begin()) + return; + + AppCacheDatabase::GroupRecord existing_group; + success_ = database_->FindGroup(group_record_.group_id, &existing_group); + if (!success_) { + success_ = database_->InsertGroup(&group_record_); + } else { + DCHECK(group_record_.group_id == existing_group.group_id); + DCHECK(group_record_.manifest_url == existing_group.manifest_url); + DCHECK(group_record_.origin == existing_group.origin); + + AppCacheDatabase::CacheRecord cache; + if (database_->FindCacheForGroup(group_record_.group_id, &cache)) { + success_ = + database_->DeleteCache(cache.cache_id) && + database_->DeleteEntriesForCache(cache.cache_id) && + database_->DeleteFallbackNameSpacesForCache(cache.cache_id) && + database_->DeleteOnlineWhiteListForCache(cache.cache_id); + // TODO(michaeln): schedule to purge unused responses from the disk cache + } else { + NOTREACHED() << "A existing group without a cache is unexpected"; + } + } + + success_ = + success_ && + database_->InsertCache(&cache_record_) && + database_->InsertEntryRecords(entry_records_) && + database_->InsertFallbackNameSpaceRecords(fallback_namespace_records_)&& + database_->InsertOnlineWhiteListRecords(online_whitelist_records_) && + transaction.Commit(); +} + +void AppCacheStorageImpl::StoreGroupAndCacheTask::RunCompleted() { + if (success_) { + storage_->origins_with_groups_.insert(group_->manifest_url().GetOrigin()); + if (cache_ != group_->newest_complete_cache()) + group_->AddCache(cache_); + } + FOR_EACH_DELEGATE(delegates_, OnGroupAndNewestCacheStored(group_, success_)); + group_ = NULL; + cache_ = NULL; +} + +void AppCacheStorageImpl::StoreGroupAndCacheTask::CancelCompletion() { + // Overriden to safely drop our reference to the group and cache + // which are not thread safe refcounted. + DatabaseTask::CancelCompletion(); + group_ = NULL; + cache_ = NULL; +} + +// FindMainResponseTask ------- + +class AppCacheStorageImpl::FindMainResponseTask : public DatabaseTask { + public: + FindMainResponseTask(AppCacheStorageImpl* storage, const GURL& url, + const AppCacheWorkingSet::GroupMap* groups_in_use) + : DatabaseTask(storage), url_(url), cache_id_(kNoCacheId) { + if (groups_in_use) { + for (AppCacheWorkingSet::GroupMap::const_iterator it = + groups_in_use->begin(); + it != groups_in_use->end(); ++it) { + AppCacheGroup* group = it->second; + AppCache* cache = group->newest_complete_cache(); + if (group->is_obsolete() || !cache) + continue; + cache_ids_in_use_.insert(cache->cache_id()); + } + } + } + + virtual void Run(); + virtual void RunCompleted(); + + GURL url_; + std::set<int64> cache_ids_in_use_; + AppCacheEntry entry_; + AppCacheEntry fallback_entry_; + int64 cache_id_; + GURL manifest_url_; +}; + +namespace { + +bool SortByLength( + const AppCacheDatabase::FallbackNameSpaceRecord& lhs, + const AppCacheDatabase::FallbackNameSpaceRecord& rhs) { + return lhs.namespace_url.spec().length() > rhs.namespace_url.spec().length(); +} + +} + +void AppCacheStorageImpl::FindMainResponseTask::Run() { + // We have a bias for hits from caches that are in use. + + // TODO(michaeln): The heuristics around choosing amoungst + // multiple candidates is under specified, and just plain + // not fully understood. Refine these over time. In particular, + // * prefer candidates from newer caches + // * take into account the cache associated with the document + // that initiated the navigation + // * take into account the cache associated with the document + // currently residing in the frame being navigated + + // First look for an exact match. We don't worry about whether + // the containing cache is in-use in this loop because the + // storage class's FindResponseForMainRequest method does that + // as a pre-optimization. + std::vector<AppCacheDatabase::EntryRecord> entries; + if (database_->FindEntriesForUrl(url_, &entries) && !entries.empty()) { + std::vector<AppCacheDatabase::EntryRecord>::iterator iter; + for (iter = entries.begin(); iter < entries.end(); ++iter) { + if (iter->flags & AppCacheEntry::FOREIGN) + continue; + + AppCacheDatabase::GroupRecord group_record; + if (!database_->FindGroupForCache(iter->cache_id, &group_record)) { + NOTREACHED() << "A cache without a group is not expected."; + continue; + } + entry_ = AppCacheEntry(iter->flags, iter->response_id); + cache_id_ = iter->cache_id; + manifest_url_ = group_record.manifest_url; + return; + } + } + + // No exact matches, look at the fallback namespaces for this origin. + std::vector<AppCacheDatabase::FallbackNameSpaceRecord> fallbacks; + if (!database_->FindFallbackNameSpacesForOrigin(url_.GetOrigin(), &fallbacks) + || fallbacks.empty()) { + return; + } + + // Sort by namespace url string length, longest to shortest, + // since longer matches trump when matching a url to a namespace. + std::sort(fallbacks.begin(), fallbacks.end(), SortByLength); + + bool has_candidate = false; + GURL candidate_fallback_namespace; + std::vector<AppCacheDatabase::FallbackNameSpaceRecord>::iterator iter; + for (iter = fallbacks.begin(); iter < fallbacks.end(); ++iter) { + if (has_candidate && + (candidate_fallback_namespace.spec().length() > + iter->namespace_url.spec().length())) { + break; // Stop iterating since longer namespace prefix matches win. + } + + if (StartsWithASCII(url_.spec(), iter->namespace_url.spec(), true)) { + bool is_cache_in_use = cache_ids_in_use_.find(iter->cache_id) != + cache_ids_in_use_.end(); + + bool take_new_candidate = !has_candidate || is_cache_in_use; + + AppCacheDatabase::EntryRecord entry_record; + if (take_new_candidate && + database_->FindEntry(iter->cache_id, iter->fallback_entry_url, + &entry_record)) { + AppCacheDatabase::GroupRecord group_record; + if (!database_->FindGroupForCache(iter->cache_id, &group_record)) { + NOTREACHED() << "A cache without a group is not expected."; + continue; + } + cache_id_ = iter->cache_id; + manifest_url_ = group_record.manifest_url; + fallback_entry_ = AppCacheEntry( + entry_record.flags, entry_record.response_id); + if (is_cache_in_use) + break; // Stop iterating since we favor hits from in-use caches. + candidate_fallback_namespace = iter->namespace_url; + has_candidate = true; + } + } + } +} + +void AppCacheStorageImpl::FindMainResponseTask::RunCompleted() { + FOR_EACH_DELEGATE(delegates_, + OnMainResponseFound(url_, entry_, fallback_entry_, + cache_id_, manifest_url_)); +} + +// MarkEntryAsForeignTask ------- + +class AppCacheStorageImpl::MarkEntryAsForeignTask : public DatabaseTask { + public: + MarkEntryAsForeignTask( + AppCacheStorageImpl* storage, const GURL& url, int64 cache_id) + : DatabaseTask(storage), cache_id_(cache_id), entry_url_(url) {} + + virtual void Run(); + virtual void RunCompleted(); + + int64 cache_id_; + GURL entry_url_; +}; + +void AppCacheStorageImpl::MarkEntryAsForeignTask::Run() { + database_->AddEntryFlags(entry_url_, cache_id_, AppCacheEntry::FOREIGN); +} + +void AppCacheStorageImpl::MarkEntryAsForeignTask::RunCompleted() { + DCHECK(storage_->pending_foreign_markings_.front().first == entry_url_ && + storage_->pending_foreign_markings_.front().second == cache_id_); + storage_->pending_foreign_markings_.pop_front(); +} + +// MakeGroupObsoleteTask ------- + +class AppCacheStorageImpl::MakeGroupObsoleteTask : public DatabaseTask { + public: + MakeGroupObsoleteTask(AppCacheStorageImpl* storage, AppCacheGroup* group); + + virtual void Run(); + virtual void RunCompleted(); + virtual void CancelCompletion(); + + scoped_refptr<AppCacheGroup> group_; + int64 group_id_; + bool success_; + std::set<GURL> origins_with_groups_; +}; + +AppCacheStorageImpl::MakeGroupObsoleteTask::MakeGroupObsoleteTask( + AppCacheStorageImpl* storage, AppCacheGroup* group) + : DatabaseTask(storage), group_(group), group_id_(group->group_id()), + success_(false) { +} + +void AppCacheStorageImpl::MakeGroupObsoleteTask::Run() { + DCHECK(!success_); + sql::Connection* connection = database_->db_connection(); + if (!connection) + return; + + sql::Transaction transaction(connection); + if (!transaction.Begin()) + return; + + AppCacheDatabase::GroupRecord group_record; + if (!database_->FindGroup(group_id_, &group_record)) { + // This group doesn't exists in the database, nothing todo here. + success_ = true; + return; + } + + AppCacheDatabase::CacheRecord cache_record; + if (database_->FindCacheForGroup(group_id_, &cache_record)) { + success_ = + database_->DeleteGroup(group_id_) && + database_->DeleteCache(cache_record.cache_id) && + database_->DeleteEntriesForCache(cache_record.cache_id) && + database_->DeleteFallbackNameSpacesForCache(cache_record.cache_id) && + database_->DeleteOnlineWhiteListForCache(cache_record.cache_id); + } else { + NOTREACHED() << "A existing group without a cache is unexpected"; + success_ = database_->DeleteGroup(group_id_); + } + + success_ = success_ && + database_->FindOriginsWithGroups(&origins_with_groups_) && + transaction.Commit(); + + // TODO(michaeln): schedule to purge unused responses from the disk cache +} + +void AppCacheStorageImpl::MakeGroupObsoleteTask::RunCompleted() { + if (success_) { + storage_->origins_with_groups_.swap(origins_with_groups_); + group_->set_obsolete(true); + } + FOR_EACH_DELEGATE(delegates_, OnGroupMadeObsolete(group_, success_)); + group_ = NULL; +} + +void AppCacheStorageImpl::MakeGroupObsoleteTask::CancelCompletion() { + // Overriden to safely drop our reference to the group + // which is not thread safe refcounted. + DatabaseTask::CancelCompletion(); + group_ = NULL; +} + + +// AppCacheStorageImpl --------------------------------------------------- + +AppCacheStorageImpl::AppCacheStorageImpl(AppCacheService* service) + : AppCacheStorage(service), is_incognito_(false), + ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { +} + +AppCacheStorageImpl::~AppCacheStorageImpl() { + STLDeleteElements(&pending_simple_tasks_); + + std::for_each(scheduled_database_tasks_.begin(), + scheduled_database_tasks_.end(), + std::mem_fun(&DatabaseTask::CancelCompletion)); + + if (database_) + AppCacheThread::DeleteSoon(AppCacheThread::db(), FROM_HERE, database_); +} + void AppCacheStorageImpl::Initialize(const FilePath& cache_directory) { - is_incognito_ = cache_directory.empty(); - cache_directory_ = cache_directory; - // TODO(michaeln): retrieve last_ids from storage + // TODO(michaeln): until purging of responses is addressed in some way, + // always use incognito mode which doesn't put anything to disk. + // Uncomment the following line when responses are dealt with. + // cache_directory_ = cache_directory; + is_incognito_ = cache_directory_.empty(); + + FilePath db_file_path; + if (!is_incognito_) + db_file_path = cache_directory.AppendASCII(kAppCacheDatabaseName); + database_ = new AppCacheDatabase(db_file_path); + + scoped_refptr<InitTask> task = new InitTask(this); + task->Schedule(); +} + +void AppCacheStorageImpl::LoadCache(int64 id, Delegate* delegate) { + DCHECK(delegate); + AppCache* cache = working_set_.GetCache(id); + if (cache) { + delegate->OnCacheLoaded(cache, id); + return; + } + scoped_refptr<CacheLoadTask> task = GetPendingCacheLoadTask(id); + if (task) { + task->AddDelegate(GetOrCreateDelegateReference(delegate)); + return; + } + task = new CacheLoadTask(id, this); + task->AddDelegate(GetOrCreateDelegateReference(delegate)); + task->Schedule(); + pending_cache_loads_[id] = task; +} + +void AppCacheStorageImpl::LoadOrCreateGroup( + const GURL& manifest_url, Delegate* delegate) { + DCHECK(delegate); + AppCacheGroup* group = working_set_.GetGroup(manifest_url); + if (group) { + delegate->OnGroupLoaded(group, manifest_url); + return; + } + + scoped_refptr<GroupLoadTask> task = GetPendingGroupLoadTask(manifest_url); + if (task) { + task->AddDelegate(GetOrCreateDelegateReference(delegate)); + return; + } + + if (origins_with_groups_.find(manifest_url.GetOrigin()) == + origins_with_groups_.end()) { + // No need to query the database, return NULL immediately. + scoped_refptr<AppCacheGroup> group = new AppCacheGroup( + service_, manifest_url, NewGroupId()); + delegate->OnGroupLoaded(group, manifest_url); + return; + } + + task = new GroupLoadTask(manifest_url, this); + task->AddDelegate(GetOrCreateDelegateReference(delegate)); + task->Schedule(); + pending_group_loads_[manifest_url] = task.get(); +} + +void AppCacheStorageImpl::StoreGroupAndNewestCache( + AppCacheGroup* group, AppCache* newest_cache, Delegate* delegate) { + // TODO(michaeln): distinguish between a simple update of an existing + // cache that just adds new master entry(s), and the insertion of a + // whole new cache. The StoreGroupAndCacheTask as written will handle + // the simple update case in a very heavy weight way (delete all and + // the reinsert all over again). + DCHECK(group && delegate); + DCHECK(newest_cache && newest_cache->is_complete()); + scoped_refptr<StoreGroupAndCacheTask> task = + new StoreGroupAndCacheTask(this, group, newest_cache); + task->AddDelegate(GetOrCreateDelegateReference(delegate)); + task->Schedule(); +} + +void AppCacheStorageImpl::FindResponseForMainRequest( + const GURL& url, Delegate* delegate) { + DCHECK(delegate); + + const GURL* url_ptr = &url; + GURL url_no_ref; + if (url.has_ref()) { + GURL::Replacements replacements; + replacements.ClearRef(); + url_no_ref = url.ReplaceComponents(replacements); + url_ptr = &url_no_ref; + } + + // First look in our working set for a direct hit without having to query + // the database. + const AppCacheWorkingSet::GroupMap* groups_in_use = + working_set()->GetGroupsInOrigin(url_ptr->GetOrigin()); + if (groups_in_use) { + for (AppCacheWorkingSet::GroupMap::const_iterator it = + groups_in_use->begin(); + it != groups_in_use->end(); ++it) { + AppCacheGroup* group = it->second; + AppCache* cache = group->newest_complete_cache(); + if (group->is_obsolete() || !cache) + continue; + + AppCacheEntry* entry = cache->GetEntry(*url_ptr); + if (entry && !entry->IsForeign()) { + ScheduleSimpleTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImpl::DeliverShortCircuitedFindMainResponse, + url, *entry, make_scoped_refptr(group), make_scoped_refptr(cache), + make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); + return; + } + } + } + + if (origins_with_groups_.find(url.GetOrigin()) == + origins_with_groups_.end()) { + // No need to query the database, return async'ly but without going thru + // the DB thread. + scoped_refptr<AppCacheGroup> no_group; + scoped_refptr<AppCache> no_cache; + ScheduleSimpleTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImpl::DeliverShortCircuitedFindMainResponse, + url, AppCacheEntry(), no_group, no_cache, + make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); + return; + } + + // We have to query the database, schedule a database task to do so. + scoped_refptr<FindMainResponseTask> task = + new FindMainResponseTask(this, *url_ptr, groups_in_use); + task->AddDelegate(GetOrCreateDelegateReference(delegate)); + task->Schedule(); +} + +void AppCacheStorageImpl::DeliverShortCircuitedFindMainResponse( + const GURL& url, AppCacheEntry found_entry, + scoped_refptr<AppCacheGroup> group, scoped_refptr<AppCache> cache, + scoped_refptr<DelegateReference> delegate_ref) { + if (delegate_ref->delegate) { + delegate_ref->delegate->OnMainResponseFound( + url, found_entry, AppCacheEntry(), + cache.get() ? cache->cache_id() : kNoCacheId, + group.get() ? group->manifest_url() : GURL::EmptyGURL()); + } +} + +void AppCacheStorageImpl::FindResponseForSubRequest( + AppCache* cache, const GURL& url, + AppCacheEntry* found_entry, AppCacheEntry* found_fallback_entry, + bool* found_network_namespace) { + DCHECK(cache && cache->is_complete()); + GURL fallback_namespace_not_used; + cache->FindResponseForRequest( + url, found_entry, found_fallback_entry, + &fallback_namespace_not_used, found_network_namespace); +} + +void AppCacheStorageImpl::MarkEntryAsForeign( + const GURL& entry_url, int64 cache_id) { + AppCache* cache = working_set_.GetCache(cache_id); + if (cache) { + AppCacheEntry* entry = cache->GetEntry(entry_url); + DCHECK(entry); + if (entry) + entry->add_types(AppCacheEntry::FOREIGN); + } + scoped_refptr<MarkEntryAsForeignTask> task = + new MarkEntryAsForeignTask(this, entry_url, cache_id); + task->Schedule(); + pending_foreign_markings_.push_back(std::make_pair(entry_url, cache_id)); +} + +void AppCacheStorageImpl::MakeGroupObsolete( + AppCacheGroup* group, Delegate* delegate) { + DCHECK(group && delegate); + scoped_refptr<MakeGroupObsoleteTask> task = + new MakeGroupObsoleteTask(this, group); + task->AddDelegate(GetOrCreateDelegateReference(delegate)); + task->Schedule(); +} + +AppCacheResponseReader* AppCacheStorageImpl::CreateResponseReader( + const GURL& manifest_url, int64 response_id) { + return new AppCacheResponseReader(response_id, disk_cache()); +} + +AppCacheResponseWriter* AppCacheStorageImpl::CreateResponseWriter( + const GURL& manifest_url) { + return new AppCacheResponseWriter(NewResponseId(), disk_cache()); +} + +void AppCacheStorageImpl::DoomResponses( + const GURL& manifest_url, const std::vector<int64>& response_ids) { + // TODO(michaeln): do something here when deleting responses +} + +AppCacheStorageImpl::CacheLoadTask* +AppCacheStorageImpl::GetPendingCacheLoadTask(int64 cache_id) { + PendingCacheLoads::iterator found = pending_cache_loads_.find(cache_id); + if (found != pending_cache_loads_.end()) + return found->second; + return NULL; +} + +AppCacheStorageImpl::GroupLoadTask* +AppCacheStorageImpl::GetPendingGroupLoadTask(const GURL& manifest_url) { + PendingGroupLoads::iterator found = pending_group_loads_.find(manifest_url); + if (found != pending_group_loads_.end()) + return found->second; + return NULL; +} + +void AppCacheStorageImpl::GetPendingForeignMarkingsForCache( + int64 cache_id, std::vector<GURL>* urls) { + PendingForeignMarkings::iterator iter = pending_foreign_markings_.begin(); + while (iter != pending_foreign_markings_.end()) { + if (iter->second == cache_id) + urls->push_back(iter->first); + ++iter; + } +} + +void AppCacheStorageImpl::ScheduleSimpleTask(Task* task) { + pending_simple_tasks_.push_back(task); + MessageLoop::current()->PostTask(FROM_HERE, + method_factory_.NewRunnableMethod( + &AppCacheStorageImpl::RunOnePendingSimpleTask)); +} + +void AppCacheStorageImpl::RunOnePendingSimpleTask() { + DCHECK(!pending_simple_tasks_.empty()); + Task* task = pending_simple_tasks_.front(); + pending_simple_tasks_.pop_front(); + task->Run(); + delete task; +} + +disk_cache::Backend* AppCacheStorageImpl::disk_cache() { + if (!disk_cache_.get()) { + if (is_incognito_) { + disk_cache_.reset( + disk_cache::CreateInMemoryCacheBackend(kMaxDiskCacheSize)); + } else { + // TODO(michaeln): create a disk backed backend + disk_cache_.reset( + disk_cache::CreateInMemoryCacheBackend(kMaxDiskCacheSize)); + } + } + return disk_cache_.get(); } } // namespace appcache diff --git a/webkit/appcache/appcache_storage_impl.h b/webkit/appcache/appcache_storage_impl.h index ef75d1e..9debc1c 100644 --- a/webkit/appcache/appcache_storage_impl.h +++ b/webkit/appcache/appcache_storage_impl.h @@ -5,22 +5,102 @@ #ifndef WEBKIT_APPCACHE_APPCACHE_STORAGE_IMPL_H_ #define WEBKIT_APPCACHE_APPCACHE_STORAGE_IMPL_H_ +#include <deque> +#include <map> +#include <set> +#include <vector> + #include "base/file_path.h" -#include "webkit/appcache/mock_appcache_storage.h" +#include "net/disk_cache/disk_cache.h" +#include "webkit/appcache/appcache_database.h" +#include "webkit/appcache/appcache_storage.h" namespace appcache { -// TODO(michaeln): write me, for now we derive from 'mock' storage. -class AppCacheStorageImpl : public MockAppCacheStorage { +class AppCacheStorageImpl : public AppCacheStorage { public: - explicit AppCacheStorageImpl(AppCacheService* service) - : MockAppCacheStorage(service), is_incognito_(false) {} + explicit AppCacheStorageImpl(AppCacheService* service); + virtual ~AppCacheStorageImpl(); void Initialize(const FilePath& cache_directory); + // AppCacheStorage methods + virtual void LoadCache(int64 id, Delegate* delegate); + virtual void LoadOrCreateGroup(const GURL& manifest_url, Delegate* delegate); + virtual void StoreGroupAndNewestCache( + AppCacheGroup* group, AppCache* newest_cache, Delegate* delegate); + virtual void FindResponseForMainRequest(const GURL& url, Delegate* delegate); + virtual void FindResponseForSubRequest( + AppCache* cache, const GURL& url, + AppCacheEntry* found_entry, AppCacheEntry* found_fallback_entry, + bool* found_network_namespace); + virtual void MarkEntryAsForeign(const GURL& entry_url, int64 cache_id); + virtual void MakeGroupObsolete(AppCacheGroup* group, Delegate* delegate); + virtual AppCacheResponseReader* CreateResponseReader( + const GURL& manifest_url, int64 response_id); + virtual AppCacheResponseWriter* CreateResponseWriter( + const GURL& manifest_url); + virtual void DoomResponses( + const GURL& manifest_url, const std::vector<int64>& response_ids); + private: - bool is_incognito_; + friend class AppCacheStorageImplTest; + + // A handful of tasks used to perform database operations on the + // background database thread. + class DatabaseTask; + class InitTask; + class CloseConnectionTask; + class StoreOrLoadTask; + class CacheLoadTask; + class GroupLoadTask; + class StoreGroupAndCacheTask; + class FindMainResponseTask; + class MarkEntryAsForeignTask; + class MakeGroupObsoleteTask; + + typedef std::deque<DatabaseTask*> DatabaseTaskQueue; + typedef std::map<int64, CacheLoadTask*> PendingCacheLoads; + typedef std::map<GURL, GroupLoadTask*> PendingGroupLoads; + typedef std::deque<std::pair<GURL, int64> > PendingForeignMarkings; + + CacheLoadTask* GetPendingCacheLoadTask(int64 cache_id); + GroupLoadTask* GetPendingGroupLoadTask(const GURL& manifest_url); + void GetPendingForeignMarkingsForCache( + int64 cache_id, std::vector<GURL>* urls); + + void ScheduleSimpleTask(Task* task); + void RunOnePendingSimpleTask(); + + // Sometimes we can respond without having to query the database. + void DeliverShortCircuitedFindMainResponse( + const GURL& url, AppCacheEntry found_entry, + scoped_refptr<AppCacheGroup> group, scoped_refptr<AppCache> newest_cache, + scoped_refptr<DelegateReference> delegate_ref); + + disk_cache::Backend* disk_cache(); + + // The directory in which we place files in the file system. FilePath cache_directory_; + bool is_incognito_; + + // Structures to keep track of DatabaseTasks that are in-flight. + DatabaseTaskQueue scheduled_database_tasks_; + PendingCacheLoads pending_cache_loads_; + PendingGroupLoads pending_group_loads_; + PendingForeignMarkings pending_foreign_markings_; + + // Created on the IO thread, but only used on the DB thread. + AppCacheDatabase* database_; + + // TODO(michaeln): use a disk_cache per group (manifest or group_id). + scoped_ptr<disk_cache::Backend> disk_cache_; + + // Used to short-circuit certain operations without having to schedule + // any tasks on the background database thread. + std::set<GURL> origins_with_groups_; + std::deque<Task*> pending_simple_tasks_; + ScopedRunnableMethodFactory<AppCacheStorageImpl> method_factory_; }; } // namespace appcache diff --git a/webkit/appcache/appcache_storage_impl_unittest.cc b/webkit/appcache/appcache_storage_impl_unittest.cc new file mode 100644 index 0000000..1db4aa8 --- /dev/null +++ b/webkit/appcache/appcache_storage_impl_unittest.cc @@ -0,0 +1,1007 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/message_loop.h" +#include "base/thread.h" +#include "base/waitable_event.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "webkit/appcache/appcache.h" +#include "webkit/appcache/appcache_database.h" +#include "webkit/appcache/appcache_entry.h" +#include "webkit/appcache/appcache_group.h" +#include "webkit/appcache/appcache_service.h" +#include "webkit/appcache/appcache_storage_impl.h" +#include "webkit/tools/test_shell/simple_appcache_system.h" + +namespace appcache { + +namespace { + +const base::TimeTicks kZeroTimeTicks; +const GURL kManifestUrl("http://blah/manifest"); +const GURL kManifestUrl2("http://blah/manifest2"); +const GURL kEntryUrl("http://blah/entry"); +const GURL kEntryUrl2("http://blah/entry2"); +const GURL kFallbackNamespace("http://blah/fallback_namespace/"); +const GURL kFallbackNamespace2("http://blah/fallback_namespace/longer"); +const GURL kFallbackTestUrl("http://blah/fallback_namespace/longer/test"); +const GURL kOnlineNamespace("http://blah/online_namespace"); + +// For the duration of this test case, we hijack the AppCacheThread API +// calls and implement them in terms of the io and db threads created here. + +scoped_ptr<base::Thread> io_thread; +scoped_ptr<base::Thread> db_thread; + +class TestThreadProvider : public SimpleAppCacheSystem::ThreadProvider { + public: + virtual bool PostTask( + int id, + const tracked_objects::Location& from_here, + Task* task) { + GetMessageLoop(id)->PostTask(from_here, task); + return true; + } + + virtual bool CurrentlyOn(int id) { + return MessageLoop::current() == GetMessageLoop(id); + } + + MessageLoop* GetMessageLoop(int id) { + DCHECK(io_thread.get() && db_thread.get()); + if (id == SimpleAppCacheSystem::IO_THREAD_ID) + return io_thread->message_loop(); + if (id == SimpleAppCacheSystem::DB_THREAD_ID) + return db_thread->message_loop(); + NOTREACHED() << "Invalid AppCacheThreadID value"; + return NULL; + } +}; + +TestThreadProvider thread_provider; + +} // namespace + +class AppCacheStorageImplTest : public testing::Test { + public: + class MockStorageDelegate : public AppCacheStorage::Delegate { + public: + explicit MockStorageDelegate(AppCacheStorageImplTest* test) + : loaded_cache_id_(0), stored_group_success_(false), + obsoleted_success_(false), found_cache_id_(kNoCacheId), + test_(test) { + } + + void OnCacheLoaded(AppCache* cache, int64 cache_id) { + loaded_cache_ = cache; + loaded_cache_id_ = cache_id; + test_->ScheduleNextTask(); + } + + void OnGroupLoaded(AppCacheGroup* group, const GURL& manifest_url) { + loaded_group_ = group; + loaded_manifest_url_ = manifest_url; + loaded_groups_newest_cache_ = group ? group->newest_complete_cache() + : NULL; + test_->ScheduleNextTask(); + } + + void OnGroupAndNewestCacheStored(AppCacheGroup* group, bool success) { + stored_group_ = group; + stored_group_success_ = success; + test_->ScheduleNextTask(); + } + + void OnGroupMadeObsolete(AppCacheGroup* group, bool success) { + obsoleted_group_ = group; + obsoleted_success_ = success; + test_->ScheduleNextTask(); + } + + void OnMainResponseFound(const GURL& url, const AppCacheEntry& entry, + const AppCacheEntry& fallback_entry, + int64 cache_id, const GURL& manifest_url) { + found_url_ = url; + found_entry_ = entry; + found_fallback_entry_ = fallback_entry; + found_cache_id_ = cache_id; + found_manifest_url_ = manifest_url; + test_->ScheduleNextTask(); + } + + scoped_refptr<AppCache> loaded_cache_; + int64 loaded_cache_id_; + scoped_refptr<AppCacheGroup> loaded_group_; + GURL loaded_manifest_url_; + scoped_refptr<AppCache> loaded_groups_newest_cache_; + scoped_refptr<AppCacheGroup> stored_group_; + bool stored_group_success_; + scoped_refptr<AppCacheGroup> obsoleted_group_; + bool obsoleted_success_; + GURL found_url_; + AppCacheEntry found_entry_; + AppCacheEntry found_fallback_entry_; + int64 found_cache_id_; + GURL found_manifest_url_; + AppCacheStorageImplTest* test_; + }; + + // Helper class run a test on our io_thread. The io_thread + // is spun up once and reused for all tests. + template <class Method> + class WrapperTask : public Task { + public: + WrapperTask(AppCacheStorageImplTest* test, Method method) + : test_(test), method_(method) { + } + + virtual void Run() { + test_->SetUpTest(); + + // Ensure InitTask execution prior to conducting a test. + test_->FlushDbThreadTasks(); + + // We also have to wait for InitTask completion call to be performed + // on the IO thread prior to running the test. Its guaranteed to be + // queued by this time. + MessageLoop::current()->PostTask(FROM_HERE, + NewRunnableFunction(&RunMethod, test_, method_)); + } + + static void RunMethod(AppCacheStorageImplTest* test, Method method) { + (test->*method)(); + } + + private: + AppCacheStorageImplTest* test_; + Method method_; + }; + + + static void SetUpTestCase() { + io_thread.reset(new base::Thread("AppCacheTest.IOThread")); + base::Thread::Options options(MessageLoop::TYPE_IO, 0); + ASSERT_TRUE(io_thread->StartWithOptions(options)); + + db_thread.reset(new base::Thread("AppCacheTest::DBThread")); + ASSERT_TRUE(db_thread->Start()); + + SimpleAppCacheSystem::set_thread_provider(&thread_provider); + } + + static void TearDownTestCase() { + SimpleAppCacheSystem::set_thread_provider(NULL); + io_thread.reset(NULL); + db_thread.reset(NULL); + } + + // Test harness -------------------------------------------------- + + AppCacheStorageImplTest() + : ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)) { + } + + template <class Method> + void RunTestOnIOThread(Method method) { + test_finished_event_ .reset(new base::WaitableEvent(false, false)); + io_thread->message_loop()->PostTask( + FROM_HERE, new WrapperTask<Method>(this, method)); + test_finished_event_->Wait(); + } + + void SetUpTest() { + DCHECK(MessageLoop::current() == io_thread->message_loop()); + service_.reset(new AppCacheService); + service_->Initialize(FilePath()); + delegate_.reset(new MockStorageDelegate(this)); + } + + void TearDownTest() { + DCHECK(MessageLoop::current() == io_thread->message_loop()); + storage()->CancelDelegateCallbacks(delegate()); + group_ = NULL; + cache_ = NULL; + cache2_ = NULL; + delegate_.reset(); + service_.reset(); + FlushDbThreadTasks(); + } + + void TestFinished() { + // We unwind the stack prior to finishing up to let stack + // based objects get deleted. + DCHECK(MessageLoop::current() == io_thread->message_loop()); + MessageLoop::current()->PostTask(FROM_HERE, + method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::TestFinishedUnwound)); + } + + void TestFinishedUnwound() { + TearDownTest(); + test_finished_event_->Signal(); + } + + void PushNextTask(Task* task) { + task_stack_.push(task); + } + + void ScheduleNextTask() { + DCHECK(MessageLoop::current() == io_thread->message_loop()); + if (task_stack_.empty()) { + return; + } + MessageLoop::current()->PostTask(FROM_HERE, task_stack_.top()); + task_stack_.pop(); + } + + static void SignalEvent(base::WaitableEvent* event) { + event->Signal(); + } + + void FlushDbThreadTasks() { + // We pump a task thru the db thread to ensure any tasks previously + // scheduled on that thread have been performed prior to return. + base::WaitableEvent event(false, false); + db_thread->message_loop()->PostTask(FROM_HERE, + NewRunnableFunction(&AppCacheStorageImplTest::SignalEvent, + &event)); + event.Wait(); + } + + // LoadCache_Miss ---------------------------------------------------- + + void LoadCache_Miss() { + // Attempt to load a cache that doesn't exist. Should + // complete asyncly. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_LoadCache_Miss)); + + storage()->LoadCache(111, delegate()); + EXPECT_NE(111, delegate()->loaded_cache_id_); + } + + void Verify_LoadCache_Miss() { + EXPECT_EQ(111, delegate()->loaded_cache_id_); + EXPECT_FALSE(delegate()->loaded_cache_); + TestFinished(); + } + + // LoadCache_NearHit ------------------------------------------------- + + void LoadCache_NearHit() { + // Attempt to load a cache that is currently in use + // and does not require loading from storage. This + // load should complete syncly. + + // Setup some preconditions. Make an 'unstored' cache for + // us to load. The ctor should put it in the working set. + int64 cache_id = storage()->NewCacheId(); + scoped_refptr<AppCache> cache = new AppCache(service(), cache_id); + + // Conduct the test. + storage()->LoadCache(cache_id, delegate()); + EXPECT_EQ(cache_id, delegate()->loaded_cache_id_); + EXPECT_EQ(cache.get(), delegate()->loaded_cache_.get()); + TestFinished(); + } + + // CreateGroup -------------------------------------------- + + void CreateGroupInEmptyOrigin() { + // Attempt to load a group that doesn't exist, one should + // be created for us, but not stored. + + // Since the origin has no groups, the storage class will respond + // syncly. + storage()->LoadOrCreateGroup(kManifestUrl, delegate()); + Verify_CreateGroup(); + } + + void CreateGroupInPopulatedOrigin() { + // Attempt to load a group that doesn't exist, one should + // be created for us, but not stored. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_CreateGroup)); + + // Since the origin has groups, storage class will have to + // consult the database and completion will be async. + storage()->origins_with_groups_.insert(kManifestUrl.GetOrigin()); + + storage()->LoadOrCreateGroup(kManifestUrl, delegate()); + EXPECT_FALSE(delegate()->loaded_group_.get()); + } + + void Verify_CreateGroup() { + EXPECT_EQ(kManifestUrl, delegate()->loaded_manifest_url_); + EXPECT_TRUE(delegate()->loaded_group_.get()); + EXPECT_TRUE(delegate()->loaded_group_->HasOneRef()); + EXPECT_FALSE(delegate()->loaded_group_->newest_complete_cache()); + + // Should not have been stored in the database. + AppCacheDatabase::GroupRecord record; + EXPECT_FALSE(database()->FindGroup( + delegate()->loaded_group_->group_id(), &record)); + + TestFinished(); + } + + // LoadGroupAndCache_FarHit -------------------------------------- + + void LoadGroupAndCache_FarHit() { + // Attempt to load a cache that is not currently in use + // and does require loading from disk. This + // load should complete asyncly. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_LoadCache_Far_Hit)); + + // Setup some preconditions. Create a group and newest cache that + // appear to be "stored" and "not currently in use". + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + group_ = NULL; + cache_ = NULL; + + // Conduct the cache load test, completes async + storage()->LoadCache(1, delegate()); + } + + void Verify_LoadCache_Far_Hit() { + EXPECT_TRUE(delegate()->loaded_cache_); + EXPECT_TRUE(delegate()->loaded_cache_->HasOneRef()); + EXPECT_EQ(1, delegate()->loaded_cache_id_); + + // The group should also have been loaded. + EXPECT_TRUE(delegate()->loaded_cache_->owning_group()); + EXPECT_TRUE(delegate()->loaded_cache_->owning_group()->HasOneRef()); + EXPECT_EQ(1, delegate()->loaded_cache_->owning_group()->group_id()); + + // Drop things from the working set. + delegate()->loaded_cache_ = NULL; + EXPECT_FALSE(delegate()->loaded_group_); + + // Conduct the group load test, also complete asyncly. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_LoadGroup_Far_Hit)); + + storage()->LoadOrCreateGroup(kManifestUrl, delegate()); + } + + void Verify_LoadGroup_Far_Hit() { + EXPECT_TRUE(delegate()->loaded_group_); + EXPECT_EQ(kManifestUrl, delegate()->loaded_manifest_url_); + EXPECT_TRUE(delegate()->loaded_group_->newest_complete_cache()); + delegate()->loaded_groups_newest_cache_ = NULL; + EXPECT_TRUE(delegate()->loaded_group_->HasOneRef()); + TestFinished(); + } + + // StoreNewGroup -------------------------------------- + + void StoreNewGroup() { + // Store a group and its newest cache. Should complete asyncly. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_StoreNewGroup)); + + // Setup some preconditions. Create a group and newest cache that + // appear to be "unstored". + group_ = new AppCacheGroup( + service(), kManifestUrl, storage()->NewGroupId()); + cache_ = new AppCache(service(), storage()->NewCacheId()); + cache_->set_complete(true); + // Hold a ref to the cache simulate the UpdateJob holding that ref, + // and hold a ref to the group to simulate the CacheHost holding that ref. + + // Conduct the store test. + storage()->StoreGroupAndNewestCache(group_, cache_, delegate()); + EXPECT_FALSE(delegate()->stored_group_success_); + } + + void Verify_StoreNewGroup() { + EXPECT_TRUE(delegate()->stored_group_success_); + EXPECT_EQ(group_.get(), delegate()->stored_group_.get()); + EXPECT_EQ(cache_.get(), group_->newest_complete_cache()); + + // Should have been stored in the database. + AppCacheDatabase::GroupRecord group_record; + AppCacheDatabase::CacheRecord cache_record; + EXPECT_TRUE(database()->FindGroup(group_->group_id(), &group_record)); + EXPECT_TRUE(database()->FindCache(cache_->cache_id(), &cache_record)); + TestFinished(); + } + + // StoreExistingGroup -------------------------------------- + + void StoreExistingGroup() { + // Store a group and its newest cache. Should complete asyncly. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_StoreExistingGroup)); + + // Setup some preconditions. Create a group and old complete cache + // that appear to be "stored" + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + + // And a newest unstored complete cache. + cache2_ = new AppCache(service(), 2); + cache2_->set_complete(true); + + // Conduct the test. + storage()->StoreGroupAndNewestCache(group_, cache2_, delegate()); + EXPECT_FALSE(delegate()->stored_group_success_); + } + + void Verify_StoreExistingGroup() { + EXPECT_TRUE(delegate()->stored_group_success_); + EXPECT_EQ(group_.get(), delegate()->stored_group_.get()); + EXPECT_EQ(cache2_.get(), group_->newest_complete_cache()); + + // The new cache should have been stored in the database. + AppCacheDatabase::GroupRecord group_record; + AppCacheDatabase::CacheRecord cache_record; + EXPECT_TRUE(database()->FindGroup(1, &group_record)); + EXPECT_TRUE(database()->FindCache(2, &cache_record)); + + // The old cache should have been deleted + EXPECT_FALSE(database()->FindCache(1, &cache_record)); + TestFinished(); + } + + // StoreExistingGroupExistingCache ------------------------------- + + void StoreExistingGroupExistingCache() { + // Store a group with updates to its existing newest complete cache. + // Setup some preconditions. Create a group and a complete cache that + // appear to be "stored". + + // Setup some preconditions. Create a group and old complete cache + // that appear to be "stored" + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + + // Change the cache. + base::TimeTicks now = base::TimeTicks::Now(); + cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::MASTER)); + cache_->set_update_time(now); + + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_StoreExistingGroupExistingCache, + now)); + + // Conduct the test. + EXPECT_EQ(cache_, group_->newest_complete_cache()); + storage()->StoreGroupAndNewestCache(group_, cache_, delegate()); + EXPECT_FALSE(delegate()->stored_group_success_); + } + + void Verify_StoreExistingGroupExistingCache( + base::TimeTicks expected_update_time) { + EXPECT_TRUE(delegate()->stored_group_success_); + EXPECT_EQ(cache_, group_->newest_complete_cache()); + + AppCacheDatabase::CacheRecord cache_record; + EXPECT_TRUE(database()->FindCache(1, &cache_record)); + EXPECT_EQ(1, cache_record.cache_id); + EXPECT_EQ(1, cache_record.group_id); + EXPECT_FALSE(cache_record.online_wildcard); + EXPECT_TRUE(expected_update_time == cache_record.update_time); + + std::vector<AppCacheDatabase::EntryRecord> entry_records; + EXPECT_TRUE(database()->FindEntriesForCache(1, &entry_records)); + EXPECT_EQ(1U, entry_records.size()); + EXPECT_EQ(1 , entry_records[0].cache_id); + EXPECT_EQ(kEntryUrl, entry_records[0].url); + EXPECT_EQ(AppCacheEntry::MASTER, entry_records[0].flags); + EXPECT_EQ(0, entry_records[0].response_id); + + TestFinished(); + } + + // MakeGroupObsolete ------------------------------- + + void MakeGroupObsolete() { + // Make a group obsolete, should complete asyncly. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_MakeGroupObsolete)); + + // Setup some preconditions. Create a group and newest cache that + // appears to be "stored" and "currently in use". + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + EXPECT_FALSE(storage()->origins_with_groups_.empty()); + + // Also insert some related records. + AppCacheDatabase::EntryRecord entry_record; + entry_record.cache_id = 1; + entry_record.flags = AppCacheEntry::FALLBACK; + entry_record.response_id = 1; + entry_record.url = kEntryUrl; + EXPECT_TRUE(database()->InsertEntry(&entry_record)); + + AppCacheDatabase::FallbackNameSpaceRecord fallback_namespace_record; + fallback_namespace_record.cache_id = 1; + fallback_namespace_record.fallback_entry_url = kEntryUrl; + fallback_namespace_record.namespace_url = kFallbackNamespace; + fallback_namespace_record.origin = kManifestUrl.GetOrigin(); + EXPECT_TRUE( + database()->InsertFallbackNameSpace(&fallback_namespace_record)); + + AppCacheDatabase::OnlineWhiteListRecord online_whitelist_record; + online_whitelist_record.cache_id = 1; + online_whitelist_record.namespace_url = kOnlineNamespace; + EXPECT_TRUE(database()->InsertOnlineWhiteList(&online_whitelist_record)); + + // Conduct the test. + storage()->MakeGroupObsolete(group_, delegate()); + EXPECT_FALSE(group_->is_obsolete()); + } + + void Verify_MakeGroupObsolete() { + EXPECT_TRUE(delegate()->obsoleted_success_); + EXPECT_EQ(group_.get(), delegate()->obsoleted_group_.get()); + EXPECT_TRUE(group_->is_obsolete()); + EXPECT_TRUE(storage()->origins_with_groups_.empty()); + + // The cache and group have been deleted from the database. + AppCacheDatabase::GroupRecord group_record; + AppCacheDatabase::CacheRecord cache_record; + EXPECT_FALSE(database()->FindGroup(1, &group_record)); + EXPECT_FALSE(database()->FindCache(1, &cache_record)); + + // The related records should have been deleted too. + std::vector<AppCacheDatabase::EntryRecord> entry_records; + database()->FindEntriesForCache(1, &entry_records); + EXPECT_TRUE(entry_records.empty()); + std::vector<AppCacheDatabase::FallbackNameSpaceRecord> fallback_records; + database()->FindFallbackNameSpacesForCache(1, &fallback_records); + EXPECT_TRUE(fallback_records.empty()); + std::vector<AppCacheDatabase::OnlineWhiteListRecord> whitelist_records; + database()->FindOnlineWhiteListForCache(1, &whitelist_records); + EXPECT_TRUE(whitelist_records.empty()); + + TestFinished(); + } + + // MarkEntryAsForeign ------------------------------- + + void MarkEntryAsForeign() { + // Setup some preconditions. Create a cache with an entry + // in storage and in the working set. + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT)); + AppCacheDatabase::EntryRecord entry_record; + entry_record.cache_id = 1; + entry_record.url = kEntryUrl; + entry_record.flags = AppCacheEntry::EXPLICIT; + entry_record.response_id = 0; + EXPECT_TRUE(database()->InsertEntry(&entry_record)); + EXPECT_FALSE(cache_->GetEntry(kEntryUrl)->IsForeign()); + + // Conduct the test. + storage()->MarkEntryAsForeign(kEntryUrl, 1); + + // The entry in the working set should have been updated syncly. + EXPECT_TRUE(cache_->GetEntry(kEntryUrl)->IsForeign()); + EXPECT_TRUE(cache_->GetEntry(kEntryUrl)->IsExplicit()); + + // And the entry in storage should also be updated, but that + // happens asyncly on the db thread. + FlushDbThreadTasks(); + AppCacheDatabase::EntryRecord entry_record2; + EXPECT_TRUE(database()->FindEntry(1, kEntryUrl, &entry_record2)); + EXPECT_EQ(AppCacheEntry::EXPLICIT | AppCacheEntry::FOREIGN, + entry_record2.flags); + TestFinished(); + } + + // MarkEntryAsForeignWithLoadInProgress ------------------------------- + + void MarkEntryAsForeignWithLoadInProgress() { + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_MarkEntryAsForeignWithLoadInProgress)); + + // Setup some preconditions. Create a cache with an entry + // in storage, but not in the working set. + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT)); + AppCacheDatabase::EntryRecord entry_record; + entry_record.cache_id = 1; + entry_record.url = kEntryUrl; + entry_record.flags = AppCacheEntry::EXPLICIT; + entry_record.response_id = 0; + EXPECT_TRUE(database()->InsertEntry(&entry_record)); + EXPECT_FALSE(cache_->GetEntry(kEntryUrl)->IsForeign()); + EXPECT_TRUE(cache_->HasOneRef()); + cache_ = NULL; + group_ = NULL; + + // Conduct the test, start a cache load, and prior to completion + // of that load, mark the entry as foreign. + storage()->LoadCache(1, delegate()); + storage()->MarkEntryAsForeign(kEntryUrl, 1); + } + + void Verify_MarkEntryAsForeignWithLoadInProgress() { + EXPECT_EQ(1, delegate()->loaded_cache_id_); + EXPECT_TRUE(delegate()->loaded_cache_.get()); + + // The entry in the working set should have been updated upon load. + EXPECT_TRUE(delegate()->loaded_cache_->GetEntry(kEntryUrl)->IsForeign()); + EXPECT_TRUE(delegate()->loaded_cache_->GetEntry(kEntryUrl)->IsExplicit()); + + // And the entry in storage should also be updated. + FlushDbThreadTasks(); + AppCacheDatabase::EntryRecord entry_record; + EXPECT_TRUE(database()->FindEntry(1, kEntryUrl, &entry_record)); + EXPECT_EQ(AppCacheEntry::EXPLICIT | AppCacheEntry::FOREIGN, + entry_record.flags); + TestFinished(); + } + + // FindNoMainResponse ------------------------------- + + void FindNoMainResponse() { + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_FindNoMainResponse)); + + // Conduct the test. + storage()->FindResponseForMainRequest(kEntryUrl, delegate()); + EXPECT_NE(kEntryUrl, delegate()->found_url_); + } + + void Verify_FindNoMainResponse() { + EXPECT_EQ(kEntryUrl, delegate()->found_url_); + EXPECT_TRUE(delegate()->found_manifest_url_.is_empty()); + EXPECT_EQ(kNoCacheId, delegate()->found_cache_id_); + EXPECT_EQ(kNoResponseId, delegate()->found_entry_.response_id()); + EXPECT_EQ(kNoResponseId, delegate()->found_fallback_entry_.response_id()); + EXPECT_EQ(0, delegate()->found_entry_.types()); + EXPECT_EQ(0, delegate()->found_fallback_entry_.types()); + TestFinished(); + } + + // BasicFindMainResponse ------------------------------- + + void BasicFindMainResponseInDatabase() { + BasicFindMainResponse(true); + } + + void BasicFindMainResponseInWorkingSet() { + BasicFindMainResponse(false); + } + + void BasicFindMainResponse(bool drop_from_working_set) { + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_BasicFindMainResponse)); + + // Setup some preconditions. Create a complete cache with an entry + // in storage. + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT, 1)); + AppCacheDatabase::EntryRecord entry_record; + entry_record.cache_id = 1; + entry_record.url = kEntryUrl; + entry_record.flags = AppCacheEntry::EXPLICIT; + entry_record.response_id = 1; + EXPECT_TRUE(database()->InsertEntry(&entry_record)); + + // Optionally drop the cache/group pair from the working set. + if (drop_from_working_set) { + EXPECT_TRUE(cache_->HasOneRef()); + cache_ = NULL; + EXPECT_TRUE(group_->HasOneRef()); + group_ = NULL; + } + + // Conduct the test. + storage()->FindResponseForMainRequest(kEntryUrl, delegate()); + EXPECT_NE(kEntryUrl, delegate()->found_url_); + } + + void Verify_BasicFindMainResponse() { + EXPECT_EQ(kEntryUrl, delegate()->found_url_); + EXPECT_EQ(kManifestUrl, delegate()->found_manifest_url_); + EXPECT_EQ(1, delegate()->found_cache_id_); + EXPECT_EQ(1, delegate()->found_entry_.response_id()); + EXPECT_TRUE(delegate()->found_entry_.IsExplicit()); + EXPECT_FALSE(delegate()->found_fallback_entry_.has_response_id()); + TestFinished(); + } + + // BasicFindMainFallbackResponse ------------------------------- + + void BasicFindMainFallbackResponseInDatabase() { + BasicFindMainFallbackResponse(true); + } + + void BasicFindMainFallbackResponseInWorkingSet() { + BasicFindMainFallbackResponse(false); + } + + void BasicFindMainFallbackResponse(bool drop_from_working_set) { + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_BasicFindMainFallbackResponse)); + + // Setup some preconditions. Create a complete cache with a + // fallback namespace and entry. + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::FALLBACK, 1)); + cache_->AddEntry(kEntryUrl2, AppCacheEntry(AppCacheEntry::FALLBACK, 2)); + cache_->fallback_namespaces_.push_back( + FallbackNamespace(kFallbackNamespace2, kEntryUrl2)); + cache_->fallback_namespaces_.push_back( + FallbackNamespace(kFallbackNamespace, kEntryUrl)); + AppCacheDatabase::CacheRecord cache_record; + std::vector<AppCacheDatabase::EntryRecord> entries; + std::vector<AppCacheDatabase::FallbackNameSpaceRecord> fallbacks; + std::vector<AppCacheDatabase::OnlineWhiteListRecord> whitelists; + cache_->ToDatabaseRecords(group_, + &cache_record, &entries, &fallbacks, &whitelists); + EXPECT_TRUE(database()->InsertEntryRecords(entries)); + EXPECT_TRUE(database()->InsertFallbackNameSpaceRecords(fallbacks)); + EXPECT_TRUE(database()->InsertOnlineWhiteListRecords(whitelists)); + if (drop_from_working_set) { + EXPECT_TRUE(cache_->HasOneRef()); + cache_ = NULL; + EXPECT_TRUE(group_->HasOneRef()); + group_ = NULL; + } + + // Conduct the test. The test url is in both fallback namespace urls, + // but should match the longer of the two. + storage()->FindResponseForMainRequest(kFallbackTestUrl, delegate()); + EXPECT_NE(kFallbackTestUrl, delegate()->found_url_); + } + + void Verify_BasicFindMainFallbackResponse() { + EXPECT_EQ(kFallbackTestUrl, delegate()->found_url_); + EXPECT_EQ(kManifestUrl, delegate()->found_manifest_url_); + EXPECT_EQ(1, delegate()->found_cache_id_); + EXPECT_FALSE(delegate()->found_entry_.has_response_id()); + EXPECT_EQ(2, delegate()->found_fallback_entry_.response_id()); + EXPECT_TRUE(delegate()->found_fallback_entry_.IsFallback()); + TestFinished(); + } + + // FindMainResponseWithMultipleHits ------------------------------- + + void FindMainResponseWithMultipleHits() { + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_FindMainResponseWithMultipleHits)); + + // Setup some preconditions. Create 2 complete caches with an entry + // for the same url. + + // The first cache, in the database but not in the working set. + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT, 1)); + AppCacheDatabase::EntryRecord entry_record; + entry_record.cache_id = 1; + entry_record.url = kEntryUrl; + entry_record.flags = AppCacheEntry::EXPLICIT; + entry_record.response_id = 1; + EXPECT_TRUE(database()->InsertEntry(&entry_record)); + cache_ = NULL; + group_ = NULL; + + // The second cache, in the database and working set. + MakeCacheAndGroup(kManifestUrl2, 2, 2, true); + cache_->AddEntry(kEntryUrl, AppCacheEntry(AppCacheEntry::EXPLICIT, 2)); + entry_record.cache_id = 2; + entry_record.url = kEntryUrl; + entry_record.flags = AppCacheEntry::EXPLICIT; + entry_record.response_id = 2; + EXPECT_TRUE(database()->InsertEntry(&entry_record)); + + // Conduct the test, we should find the response from the second cache + // since it's "in use". + storage()->FindResponseForMainRequest(kEntryUrl, delegate()); + EXPECT_NE(kEntryUrl, delegate()->found_url_); + } + + void Verify_FindMainResponseWithMultipleHits() { + EXPECT_EQ(kEntryUrl, delegate()->found_url_); + EXPECT_EQ(kManifestUrl2, delegate()->found_manifest_url_); + EXPECT_EQ(2, delegate()->found_cache_id_); + EXPECT_EQ(2, delegate()->found_entry_.response_id()); + EXPECT_TRUE(delegate()->found_entry_.IsExplicit()); + EXPECT_FALSE(delegate()->found_fallback_entry_.has_response_id()); + TestFinished(); + } + + // FindMainResponseExclusions ------------------------------- + + void FindMainResponseExclusionsInDatabase() { + FindMainResponseExclusions(true); + } + + void FindMainResponseExclusionsInWorkingSet() { + FindMainResponseExclusions(false); + } + + void FindMainResponseExclusions(bool drop_from_working_set) { + // Setup some preconditions. Create a complete cache with a + // foreign entry and an online namespace. + MakeCacheAndGroup(kManifestUrl, 1, 1, true); + cache_->AddEntry(kEntryUrl, + AppCacheEntry(AppCacheEntry::EXPLICIT | AppCacheEntry::FOREIGN, 1)); + cache_->online_whitelist_namespaces_.push_back(kOnlineNamespace); + AppCacheDatabase::EntryRecord entry_record; + entry_record.cache_id = 1; + entry_record.url = kEntryUrl; + entry_record.flags = AppCacheEntry::EXPLICIT | AppCacheEntry::FOREIGN; + entry_record.response_id = 1; + EXPECT_TRUE(database()->InsertEntry(&entry_record)); + AppCacheDatabase::OnlineWhiteListRecord whitelist_record; + whitelist_record.cache_id = 1; + whitelist_record.namespace_url = kOnlineNamespace; + EXPECT_TRUE(database()->InsertOnlineWhiteList(&whitelist_record)); + if (drop_from_working_set) { + cache_ = NULL; + group_ = NULL; + } + + // We should not find anything for the foreign entry. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_NotFound, kEntryUrl, false)); + storage()->FindResponseForMainRequest(kEntryUrl, delegate()); + } + + void Verify_NotFound(GURL expected_url, bool test_finished) { + EXPECT_EQ(expected_url, delegate()->found_url_); + EXPECT_TRUE(delegate()->found_manifest_url_.is_empty()); + EXPECT_EQ(kNoCacheId, delegate()->found_cache_id_); + EXPECT_EQ(kNoResponseId, delegate()->found_entry_.response_id()); + EXPECT_EQ(kNoResponseId, delegate()->found_fallback_entry_.response_id()); + EXPECT_EQ(0, delegate()->found_entry_.types()); + EXPECT_EQ(0, delegate()->found_fallback_entry_.types()); + + if (!test_finished) { + // We should not find anything for the online namespace. + PushNextTask(method_factory_.NewRunnableMethod( + &AppCacheStorageImplTest::Verify_NotFound, kOnlineNamespace, true)); + storage()->FindResponseForMainRequest(kOnlineNamespace, delegate()); + return; + } + + TestFinished(); + } + + // Test case helpers -------------------------------------------------- + + AppCacheService* service() { + return service_.get(); + } + + AppCacheStorageImpl* storage() { + return static_cast<AppCacheStorageImpl*>(service()->storage()); + } + + AppCacheDatabase* database() { + return storage()->database_; + } + + MockStorageDelegate* delegate() { + return delegate_.get(); + } + + void MakeCacheAndGroup( + const GURL& manifest_url, int64 group_id, int64 cache_id, + bool add_to_database) { + group_ = new AppCacheGroup(service(), manifest_url, group_id); + cache_ = new AppCache(service(), cache_id); + cache_->set_complete(true); + group_->AddCache(cache_); + if (add_to_database) { + AppCacheDatabase::GroupRecord group_record; + group_record.group_id = group_id; + group_record.manifest_url = manifest_url; + group_record.origin = manifest_url.GetOrigin(); + EXPECT_TRUE(database()->InsertGroup(&group_record)); + AppCacheDatabase::CacheRecord cache_record; + cache_record.cache_id = cache_id; + cache_record.group_id = group_id; + cache_record.online_wildcard = false; + cache_record.update_time = kZeroTimeTicks; + EXPECT_TRUE(database()->InsertCache(&cache_record)); + storage()->origins_with_groups_.insert(manifest_url.GetOrigin()); + } + } + + // Data members -------------------------------------------------- + + ScopedRunnableMethodFactory<AppCacheStorageImplTest> method_factory_; + scoped_ptr<base::WaitableEvent> test_finished_event_; + std::stack<Task*> task_stack_; + scoped_ptr<AppCacheService> service_; + scoped_ptr<MockStorageDelegate> delegate_; + scoped_refptr<AppCacheGroup> group_; + scoped_refptr<AppCache> cache_; + scoped_refptr<AppCache> cache2_; +}; + + +TEST_F(AppCacheStorageImplTest, LoadCache_Miss) { + RunTestOnIOThread(&AppCacheStorageImplTest::LoadCache_Miss); +} + +TEST_F(AppCacheStorageImplTest, LoadCache_NearHit) { + RunTestOnIOThread(&AppCacheStorageImplTest::LoadCache_NearHit); +} + +TEST_F(AppCacheStorageImplTest, CreateGroupInEmptyOrigin) { + RunTestOnIOThread(&AppCacheStorageImplTest::CreateGroupInEmptyOrigin); +} + +TEST_F(AppCacheStorageImplTest, CreateGroupInPopulatedOrigin) { + RunTestOnIOThread(&AppCacheStorageImplTest::CreateGroupInPopulatedOrigin); +} + +TEST_F(AppCacheStorageImplTest, LoadGroupAndCache_FarHit) { + RunTestOnIOThread(&AppCacheStorageImplTest::LoadGroupAndCache_FarHit); +} + +TEST_F(AppCacheStorageImplTest, StoreNewGroup) { + RunTestOnIOThread(&AppCacheStorageImplTest::StoreNewGroup); +} + +TEST_F(AppCacheStorageImplTest, StoreExistingGroup) { + RunTestOnIOThread(&AppCacheStorageImplTest::StoreExistingGroup); +} + +TEST_F(AppCacheStorageImplTest, StoreExistingGroupExistingCache) { + RunTestOnIOThread(&AppCacheStorageImplTest::StoreExistingGroupExistingCache); +} + +TEST_F(AppCacheStorageImplTest, MakeGroupObsolete) { + RunTestOnIOThread(&AppCacheStorageImplTest::MakeGroupObsolete); +} + +TEST_F(AppCacheStorageImplTest, MarkEntryAsForeign) { + RunTestOnIOThread(&AppCacheStorageImplTest::MarkEntryAsForeign); +} + +TEST_F(AppCacheStorageImplTest, MarkEntryAsForeignWithLoadInProgress) { + RunTestOnIOThread( + &AppCacheStorageImplTest::MarkEntryAsForeignWithLoadInProgress); +} + +TEST_F(AppCacheStorageImplTest, FindNoMainResponse) { + RunTestOnIOThread(&AppCacheStorageImplTest::FindNoMainResponse); +} + +TEST_F(AppCacheStorageImplTest, BasicFindMainResponseInDatabase) { + RunTestOnIOThread( + &AppCacheStorageImplTest::BasicFindMainResponseInDatabase); +} + +TEST_F(AppCacheStorageImplTest, BasicFindMainResponseInWorkingSet) { + RunTestOnIOThread( + &AppCacheStorageImplTest::BasicFindMainResponseInWorkingSet); +} + +TEST_F(AppCacheStorageImplTest, BasicFindMainFallbackResponseInDatabase) { + RunTestOnIOThread( + &AppCacheStorageImplTest::BasicFindMainFallbackResponseInDatabase); +} + +TEST_F(AppCacheStorageImplTest, BasicFindMainFallbackResponseInWorkingSet) { + RunTestOnIOThread( + &AppCacheStorageImplTest::BasicFindMainFallbackResponseInWorkingSet); +} + +TEST_F(AppCacheStorageImplTest, FindMainResponseWithMultipleHits) { + RunTestOnIOThread( + &AppCacheStorageImplTest::FindMainResponseWithMultipleHits); +} + +TEST_F(AppCacheStorageImplTest, FindMainResponseExclusionsInDatabase) { + RunTestOnIOThread( + &AppCacheStorageImplTest::FindMainResponseExclusionsInDatabase); +} + +TEST_F(AppCacheStorageImplTest, FindMainResponseExclusionsInWorkingSet) { + RunTestOnIOThread( + &AppCacheStorageImplTest::FindMainResponseExclusionsInWorkingSet); +} + +// That's all folks! + +} // namespace appcache + diff --git a/webkit/appcache/appcache_thread.h b/webkit/appcache/appcache_thread.h index 9f14b1c..7224d23 100644 --- a/webkit/appcache/appcache_thread.h +++ b/webkit/appcache/appcache_thread.h @@ -5,10 +5,11 @@ #ifndef WEBKIT_APPCACHE_APPCACHE_THREAD_H_ #define WEBKIT_APPCACHE_APPCACHE_THREAD_H_ +#include "base/task.h" + namespace tracked_objects { class Location; } -class Task; namespace appcache { @@ -31,6 +32,13 @@ class AppCacheThread { Task* task); static bool CurrentlyOn(int id); + template <class T> + static bool DeleteSoon(int id, + const tracked_objects::Location& from_here, + T* object) { + return PostTask(id, from_here, new DeleteTask<T>(object)); + } + private: AppCacheThread(); ~AppCacheThread(); diff --git a/webkit/appcache/appcache_working_set.cc b/webkit/appcache/appcache_working_set.cc index 2b61ea3..652116c 100644 --- a/webkit/appcache/appcache_working_set.cc +++ b/webkit/appcache/appcache_working_set.cc @@ -14,6 +14,7 @@ namespace appcache { AppCacheWorkingSet::~AppCacheWorkingSet() { DCHECK(caches_.empty()); DCHECK(groups_.empty()); + DCHECK(groups_by_origin_.empty()); } void AppCacheWorkingSet::AddCache(AppCache* cache) { @@ -31,10 +32,20 @@ void AppCacheWorkingSet::AddGroup(AppCacheGroup* group) { const GURL& url = group->manifest_url(); DCHECK(groups_.find(url) == groups_.end()); groups_.insert(GroupMap::value_type(url, group)); + groups_by_origin_[url.GetOrigin()].insert(GroupMap::value_type(url, group)); } void AppCacheWorkingSet::RemoveGroup(AppCacheGroup* group) { - groups_.erase(group->manifest_url()); + const GURL& url = group->manifest_url(); + groups_.erase(url); + + GURL origin_url = url.GetOrigin(); + GroupMap* groups_in_origin = GetMutableGroupsInOrigin(origin_url); + if (groups_in_origin) { + groups_in_origin->erase(url); + if (groups_in_origin->empty()) + groups_by_origin_.erase(origin_url); + } } void AppCacheWorkingSet::AddResponseInfo(AppCacheResponseInfo* info) { diff --git a/webkit/appcache/appcache_working_set.h b/webkit/appcache/appcache_working_set.h index 6785d50..479e74d 100644 --- a/webkit/appcache/appcache_working_set.h +++ b/webkit/appcache/appcache_working_set.h @@ -20,6 +20,8 @@ class AppCacheResponseInfo; // currently in memory. class AppCacheWorkingSet { public: + typedef std::map<GURL, AppCacheGroup*> GroupMap; + ~AppCacheWorkingSet(); void AddCache(AppCache* cache); @@ -36,6 +38,10 @@ class AppCacheWorkingSet { return (it != groups_.end()) ? it->second : NULL; } + const GroupMap* GetGroupsInOrigin(const GURL& origin_url) { + return GetMutableGroupsInOrigin(origin_url); + } + void AddResponseInfo(AppCacheResponseInfo* response_info); void RemoveResponseInfo(AppCacheResponseInfo* response_info); AppCacheResponseInfo* GetResponseInfo(int64 id) { @@ -45,10 +51,17 @@ class AppCacheWorkingSet { private: typedef base::hash_map<int64, AppCache*> CacheMap; - typedef std::map<GURL, AppCacheGroup*> GroupMap; + typedef std::map<GURL, GroupMap> GroupsByOriginMap; typedef base::hash_map<int64, AppCacheResponseInfo*> ResponseInfoMap; + + GroupMap* GetMutableGroupsInOrigin(const GURL& origin_url) { + GroupsByOriginMap::iterator it = groups_by_origin_.find(origin_url); + return (it != groups_by_origin_.end()) ? &it->second : NULL; + } + CacheMap caches_; GroupMap groups_; + GroupsByOriginMap groups_by_origin_; // origin -> (manifest -> group) ResponseInfoMap response_infos_; }; diff --git a/webkit/appcache/mock_appcache_storage.cc b/webkit/appcache/mock_appcache_storage.cc index 214be249..44bcd12 100644 --- a/webkit/appcache/mock_appcache_storage.cc +++ b/webkit/appcache/mock_appcache_storage.cc @@ -124,8 +124,6 @@ void MockAppCacheStorage::MarkEntryAsForeign( if (entry) entry->add_types(AppCacheEntry::FOREIGN); } - // TODO(michaeln): in real storage update in storage, and if this cache is - // being loaded be sure to update the memory cache upon load completion. } void MockAppCacheStorage::MakeGroupObsolete( @@ -140,7 +138,7 @@ void MockAppCacheStorage::MakeGroupObsolete( } AppCacheResponseReader* MockAppCacheStorage::CreateResponseReader( - const GURL& origin, int64 response_id) { + const GURL& manifest_url, int64 response_id) { return new AppCacheResponseReader(response_id, disk_cache()); } @@ -203,14 +201,6 @@ void MockAppCacheStorage::ProcessStoreGroupAndNewestCache( if (delegate_ref->delegate) delegate_ref->delegate->OnGroupAndNewestCacheStored(group, true); - - // We don't bother with removing responses from 'mock' storage - // TODO(michaeln): for 'real' storage... - // std::set<int64> doomed_responses_ = responses from old caches - // std::set<int64> needed_responses_ = responses from newest cache - // foreach(needed_responses_) - // doomed_responses_.remove(needed_response_); - // DoomResponses(group->manifest_url(), doomed_responses_); } namespace { |