| // Copyright (c) 2011 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/mock_appcache_storage.h" |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/message_loop.h" |
| #include "base/stl_util.h" |
| #include "webkit/appcache/appcache.h" |
| #include "webkit/appcache/appcache_entry.h" |
| #include "webkit/appcache/appcache_group.h" |
| #include "webkit/appcache/appcache_response.h" |
| #include "webkit/appcache/appcache_service.h" |
| |
| // This is a quick and easy 'mock' implementation of the storage interface |
| // that doesn't put anything to disk. |
| // |
| // We simply add an extra reference to objects when they're put in storage, |
| // and remove the extra reference when they are removed from storage. |
| // Responses are never really removed from the in-memory disk cache. |
| // Delegate callbacks are made asyncly to appropiately mimic what will |
| // happen with a real disk-backed storage impl that involves IO on a |
| // background thread. |
| |
| namespace appcache { |
| |
| MockAppCacheStorage::MockAppCacheStorage(AppCacheService* service) |
| : AppCacheStorage(service), |
| ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)), |
| simulate_make_group_obsolete_failure_(false), |
| simulate_store_group_and_newest_cache_failure_(false), |
| simulate_find_main_resource_(false), |
| simulate_find_sub_resource_(false), |
| simulated_found_cache_id_(kNoCacheId), |
| simulated_found_group_id_(0), |
| simulated_found_network_namespace_(false) { |
| last_cache_id_ = 0; |
| last_group_id_ = 0; |
| last_response_id_ = 0; |
| } |
| |
| MockAppCacheStorage::~MockAppCacheStorage() { |
| } |
| |
| void MockAppCacheStorage::GetAllInfo(Delegate* delegate) { |
| ScheduleTask( |
| base::Bind(&MockAppCacheStorage::ProcessGetAllInfo, |
| weak_factory_.GetWeakPtr(), |
| make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); |
| } |
| |
| void MockAppCacheStorage::LoadCache(int64 id, Delegate* delegate) { |
| DCHECK(delegate); |
| AppCache* cache = working_set_.GetCache(id); |
| if (ShouldCacheLoadAppearAsync(cache)) { |
| ScheduleTask( |
| base::Bind(&MockAppCacheStorage::ProcessLoadCache, |
| weak_factory_.GetWeakPtr(), id, |
| make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); |
| return; |
| } |
| ProcessLoadCache(id, GetOrCreateDelegateReference(delegate)); |
| } |
| |
| void MockAppCacheStorage::LoadOrCreateGroup( |
| const GURL& manifest_url, Delegate* delegate) { |
| DCHECK(delegate); |
| AppCacheGroup* group = working_set_.GetGroup(manifest_url); |
| if (ShouldGroupLoadAppearAsync(group)) { |
| ScheduleTask( |
| base::Bind(&MockAppCacheStorage::ProcessLoadOrCreateGroup, |
| weak_factory_.GetWeakPtr(), manifest_url, |
| make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); |
| return; |
| } |
| ProcessLoadOrCreateGroup( |
| manifest_url, GetOrCreateDelegateReference(delegate)); |
| } |
| |
| void MockAppCacheStorage::StoreGroupAndNewestCache( |
| AppCacheGroup* group, AppCache* newest_cache, Delegate* delegate) { |
| DCHECK(group && delegate && newest_cache); |
| |
| // Always make this operation look async. |
| ScheduleTask( |
| base::Bind(&MockAppCacheStorage::ProcessStoreGroupAndNewestCache, |
| weak_factory_.GetWeakPtr(), make_scoped_refptr(group), |
| make_scoped_refptr(newest_cache), |
| make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); |
| } |
| |
| void MockAppCacheStorage::FindResponseForMainRequest( |
| const GURL& url, const GURL& preferred_manifest_url, Delegate* delegate) { |
| DCHECK(delegate); |
| |
| // Note: MockAppCacheStorage does not respect the preferred_manifest_url. |
| |
| // Always make this operation look async. |
| ScheduleTask( |
| base::Bind(&MockAppCacheStorage::ProcessFindResponseForMainRequest, |
| weak_factory_.GetWeakPtr(), url, |
| make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); |
| } |
| |
| void MockAppCacheStorage::FindResponseForSubRequest( |
| AppCache* cache, const GURL& url, |
| AppCacheEntry* found_entry, AppCacheEntry* found_fallback_entry, |
| bool* found_network_namespace) { |
| DCHECK(cache && cache->is_complete()); |
| |
| // This layer of indirection is here to facilitate testing. |
| if (simulate_find_sub_resource_) { |
| *found_entry = simulated_found_entry_; |
| *found_fallback_entry = simulated_found_fallback_entry_; |
| *found_network_namespace = simulated_found_network_namespace_; |
| simulate_find_sub_resource_ = false; |
| return; |
| } |
| |
| GURL fallback_namespace_not_used; |
| GURL intercept_namespace_not_used; |
| cache->FindResponseForRequest( |
| url, found_entry, &intercept_namespace_not_used, |
| found_fallback_entry, &fallback_namespace_not_used, |
| found_network_namespace); |
| } |
| |
| void MockAppCacheStorage::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); |
| } |
| } |
| |
| void MockAppCacheStorage::MakeGroupObsolete( |
| AppCacheGroup* group, Delegate* delegate) { |
| DCHECK(group && delegate); |
| |
| // Always make this method look async. |
| ScheduleTask( |
| base::Bind(&MockAppCacheStorage::ProcessMakeGroupObsolete, |
| weak_factory_.GetWeakPtr(), make_scoped_refptr(group), |
| make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); |
| } |
| |
| AppCacheResponseReader* MockAppCacheStorage::CreateResponseReader( |
| const GURL& manifest_url, int64 group_id, int64 response_id) { |
| if (simulated_reader_.get()) |
| return simulated_reader_.release(); |
| return new AppCacheResponseReader(response_id, group_id, disk_cache()); |
| } |
| |
| AppCacheResponseWriter* MockAppCacheStorage::CreateResponseWriter( |
| const GURL& manifest_url, int64 group_id) { |
| return new AppCacheResponseWriter(NewResponseId(), group_id, disk_cache()); |
| } |
| |
| void MockAppCacheStorage::DoomResponses( |
| const GURL& manifest_url, const std::vector<int64>& response_ids) { |
| DeleteResponses(manifest_url, response_ids); |
| } |
| |
| void MockAppCacheStorage::DeleteResponses( |
| const GURL& manifest_url, const std::vector<int64>& response_ids) { |
| // We don't bother with actually removing responses from the disk-cache, |
| // just keep track of which ids have been doomed or deleted |
| std::vector<int64>::const_iterator it = response_ids.begin(); |
| while (it != response_ids.end()) { |
| doomed_response_ids_.insert(*it); |
| ++it; |
| } |
| } |
| |
| void MockAppCacheStorage::ProcessGetAllInfo( |
| scoped_refptr<DelegateReference> delegate_ref) { |
| if (delegate_ref->delegate) |
| delegate_ref->delegate->OnAllInfo(simulated_appcache_info_); |
| } |
| |
| void MockAppCacheStorage::ProcessLoadCache( |
| int64 id, scoped_refptr<DelegateReference> delegate_ref) { |
| AppCache* cache = working_set_.GetCache(id); |
| if (delegate_ref->delegate) |
| delegate_ref->delegate->OnCacheLoaded(cache, id); |
| } |
| |
| void MockAppCacheStorage::ProcessLoadOrCreateGroup( |
| const GURL& manifest_url, scoped_refptr<DelegateReference> delegate_ref) { |
| scoped_refptr<AppCacheGroup> group(working_set_.GetGroup(manifest_url)); |
| |
| // Newly created groups are not put in the stored_groups collection |
| // until StoreGroupAndNewestCache is called. |
| if (!group) |
| group = new AppCacheGroup(service_, manifest_url, NewGroupId()); |
| |
| if (delegate_ref->delegate) |
| delegate_ref->delegate->OnGroupLoaded(group, manifest_url); |
| } |
| |
| void MockAppCacheStorage::ProcessStoreGroupAndNewestCache( |
| scoped_refptr<AppCacheGroup> group, |
| scoped_refptr<AppCache> newest_cache, |
| scoped_refptr<DelegateReference> delegate_ref) { |
| Delegate* delegate = delegate_ref->delegate; |
| if (simulate_store_group_and_newest_cache_failure_) { |
| if (delegate) |
| delegate->OnGroupAndNewestCacheStored(group, newest_cache, false, false); |
| return; |
| } |
| |
| AddStoredGroup(group); |
| if (newest_cache != group->newest_complete_cache()) { |
| newest_cache->set_complete(true); |
| group->AddCache(newest_cache); |
| AddStoredCache(newest_cache); |
| |
| // Copy the collection prior to removal, on final release |
| // of a cache the group's collection will change. |
| AppCacheGroup::Caches copy = group->old_caches(); |
| RemoveStoredCaches(copy); |
| } |
| |
| if (delegate) |
| delegate->OnGroupAndNewestCacheStored(group, newest_cache, true, false); |
| } |
| |
| namespace { |
| |
| struct FoundCandidate { |
| GURL namespace_entry_url; |
| AppCacheEntry entry; |
| int64 cache_id; |
| int64 group_id; |
| GURL manifest_url; |
| bool is_cache_in_use; |
| |
| FoundCandidate() |
| : cache_id(kNoCacheId), group_id(0), is_cache_in_use(false) {} |
| }; |
| |
| void MaybeTakeNewNamespaceEntry( |
| NamespaceType namespace_type, |
| const AppCacheEntry &entry, |
| const GURL& namespace_url, |
| bool cache_is_in_use, |
| FoundCandidate* best_candidate, |
| GURL* best_candidate_namespace, |
| AppCache* cache, |
| AppCacheGroup* group) { |
| DCHECK(entry.has_response_id()); |
| |
| bool take_new_entry = true; |
| |
| // Does the new candidate entry trump our current best candidate? |
| if (best_candidate->entry.has_response_id()) { |
| // Longer namespace prefix matches win. |
| size_t candidate_length = |
| namespace_url.spec().length(); |
| size_t best_length = |
| best_candidate_namespace->spec().length(); |
| |
| if (candidate_length > best_length) { |
| take_new_entry = true; |
| } else if (candidate_length == best_length && |
| cache_is_in_use && !best_candidate->is_cache_in_use) { |
| take_new_entry = true; |
| } else { |
| take_new_entry = false; |
| } |
| } |
| |
| if (take_new_entry) { |
| if (namespace_type == FALLBACK_NAMESPACE) { |
| best_candidate->namespace_entry_url = |
| cache->GetFallbackEntryUrl(namespace_url); |
| } else { |
| best_candidate->namespace_entry_url = |
| cache->GetInterceptEntryUrl(namespace_url); |
| } |
| best_candidate->entry = entry; |
| best_candidate->cache_id = cache->cache_id(); |
| best_candidate->group_id = group->group_id(); |
| best_candidate->manifest_url = group->manifest_url(); |
| best_candidate->is_cache_in_use = cache_is_in_use; |
| *best_candidate_namespace = namespace_url; |
| } |
| } |
| } // namespace |
| |
| void MockAppCacheStorage::ProcessFindResponseForMainRequest( |
| const GURL& url, scoped_refptr<DelegateReference> delegate_ref) { |
| if (simulate_find_main_resource_) { |
| simulate_find_main_resource_ = false; |
| if (delegate_ref->delegate) { |
| delegate_ref->delegate->OnMainResponseFound( |
| url, simulated_found_entry_, |
| simulated_found_fallback_url_, simulated_found_fallback_entry_, |
| simulated_found_cache_id_, simulated_found_group_id_, |
| simulated_found_manifest_url_); |
| } |
| return; |
| } |
| |
| // This call has no persistent side effects, if the delegate has gone |
| // away, we can just bail out early. |
| if (!delegate_ref->delegate) |
| return; |
| |
| // 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 |
| FoundCandidate found_candidate; |
| GURL found_intercept_candidate_namespace; |
| FoundCandidate found_fallback_candidate; |
| GURL found_fallback_candidate_namespace; |
| |
| for (StoredGroupMap::const_iterator it = stored_groups_.begin(); |
| it != stored_groups_.end(); ++it) { |
| AppCacheGroup* group = it->second.get(); |
| AppCache* cache = group->newest_complete_cache(); |
| if (group->is_obsolete() || !cache || |
| (url.GetOrigin() != group->manifest_url().GetOrigin())) { |
| continue; |
| } |
| |
| AppCacheEntry found_entry; |
| AppCacheEntry found_fallback_entry; |
| GURL found_intercept_namespace; |
| GURL found_fallback_namespace; |
| bool ignore_found_network_namespace = false; |
| bool found = cache->FindResponseForRequest( |
| url, &found_entry, &found_intercept_namespace, |
| &found_fallback_entry, &found_fallback_namespace, |
| &ignore_found_network_namespace); |
| |
| // 6.11.1 Navigating across documents, Step 10. |
| // Network namespacing doesn't apply to main resource loads, |
| // and foreign entries are excluded. |
| if (!found || ignore_found_network_namespace || |
| (found_entry.has_response_id() && found_entry.IsForeign()) || |
| (found_fallback_entry.has_response_id() && |
| found_fallback_entry.IsForeign())) { |
| continue; |
| } |
| |
| // We have a bias for hits from caches that are in use. |
| bool is_in_use = IsCacheStored(cache) && !cache->HasOneRef(); |
| |
| if (found_entry.has_response_id() && |
| found_intercept_namespace.is_empty()) { |
| found_candidate.namespace_entry_url = GURL(); |
| found_candidate.entry = found_entry; |
| found_candidate.cache_id = cache->cache_id(); |
| found_candidate.group_id = group->group_id(); |
| found_candidate.manifest_url = group->manifest_url(); |
| found_candidate.is_cache_in_use = is_in_use; |
| if (is_in_use) |
| break; // We break out of the loop with this direct hit. |
| } else if (found_entry.has_response_id() && |
| !found_intercept_namespace.is_empty()) { |
| MaybeTakeNewNamespaceEntry( |
| INTERCEPT_NAMESPACE, |
| found_entry, found_intercept_namespace, is_in_use, |
| &found_candidate, &found_intercept_candidate_namespace, |
| cache, group); |
| } else { |
| DCHECK(found_fallback_entry.has_response_id()); |
| MaybeTakeNewNamespaceEntry( |
| FALLBACK_NAMESPACE, |
| found_fallback_entry, found_fallback_namespace, is_in_use, |
| &found_fallback_candidate, &found_fallback_candidate_namespace, |
| cache, group); |
| } |
| } |
| |
| // Found a direct hit or an intercept namespace hit. |
| if (found_candidate.entry.has_response_id()) { |
| delegate_ref->delegate->OnMainResponseFound( |
| url, found_candidate.entry, found_candidate.namespace_entry_url, |
| AppCacheEntry(), found_candidate.cache_id, found_candidate.group_id, |
| found_candidate.manifest_url); |
| return; |
| } |
| |
| // Found a fallback namespace. |
| if (found_fallback_candidate.entry.has_response_id()) { |
| delegate_ref->delegate->OnMainResponseFound( |
| url, AppCacheEntry(), |
| found_fallback_candidate.namespace_entry_url, |
| found_fallback_candidate.entry, |
| found_fallback_candidate.cache_id, |
| found_fallback_candidate.group_id, |
| found_fallback_candidate.manifest_url); |
| return; |
| } |
| |
| // Didn't find anything. |
| delegate_ref->delegate->OnMainResponseFound( |
| url, AppCacheEntry(), GURL(), AppCacheEntry(), kNoCacheId, 0, GURL()); |
| } |
| |
| void MockAppCacheStorage::ProcessMakeGroupObsolete( |
| scoped_refptr<AppCacheGroup> group, |
| scoped_refptr<DelegateReference> delegate_ref) { |
| if (simulate_make_group_obsolete_failure_) { |
| if (delegate_ref->delegate) |
| delegate_ref->delegate->OnGroupMadeObsolete(group, false); |
| return; |
| } |
| |
| RemoveStoredGroup(group); |
| if (group->newest_complete_cache()) |
| RemoveStoredCache(group->newest_complete_cache()); |
| |
| // Copy the collection prior to removal, on final release |
| // of a cache the group's collection will change. |
| AppCacheGroup::Caches copy = group->old_caches(); |
| RemoveStoredCaches(copy); |
| |
| group->set_obsolete(true); |
| |
| // Also remove from the working set, caches for an 'obsolete' group |
| // may linger in use, but the group itself cannot be looked up by |
| // 'manifest_url' in the working set any longer. |
| working_set()->RemoveGroup(group); |
| |
| if (delegate_ref->delegate) |
| delegate_ref->delegate->OnGroupMadeObsolete(group, true); |
| } |
| |
| void MockAppCacheStorage::ScheduleTask(const base::Closure& task) { |
| pending_tasks_.push_back(task); |
| MessageLoop::current()->PostTask( |
| FROM_HERE, |
| base::Bind(&MockAppCacheStorage::RunOnePendingTask, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void MockAppCacheStorage::RunOnePendingTask() { |
| DCHECK(!pending_tasks_.empty()); |
| base::Closure task = pending_tasks_.front(); |
| pending_tasks_.pop_front(); |
| task.Run(); |
| } |
| |
| void MockAppCacheStorage::AddStoredCache(AppCache* cache) { |
| int64 cache_id = cache->cache_id(); |
| if (stored_caches_.find(cache_id) == stored_caches_.end()) { |
| stored_caches_.insert( |
| StoredCacheMap::value_type(cache_id, make_scoped_refptr(cache))); |
| } |
| } |
| |
| void MockAppCacheStorage::RemoveStoredCache(AppCache* cache) { |
| // Do not remove from the working set, active caches are still usable |
| // and may be looked up by id until they fall out of use. |
| stored_caches_.erase(cache->cache_id()); |
| } |
| |
| void MockAppCacheStorage::RemoveStoredCaches( |
| const AppCacheGroup::Caches& caches) { |
| AppCacheGroup::Caches::const_iterator it = caches.begin(); |
| while (it != caches.end()) { |
| RemoveStoredCache(*it); |
| ++it; |
| } |
| } |
| |
| void MockAppCacheStorage::AddStoredGroup(AppCacheGroup* group) { |
| const GURL& url = group->manifest_url(); |
| if (stored_groups_.find(url) == stored_groups_.end()) { |
| stored_groups_.insert( |
| StoredGroupMap::value_type(url, make_scoped_refptr(group))); |
| } |
| } |
| |
| void MockAppCacheStorage::RemoveStoredGroup(AppCacheGroup* group) { |
| stored_groups_.erase(group->manifest_url()); |
| } |
| |
| bool MockAppCacheStorage::ShouldGroupLoadAppearAsync( |
| const AppCacheGroup* group) { |
| // We'll have to query the database to see if a group for the |
| // manifest_url exists on disk. So return true for async. |
| if (!group) |
| return true; |
| |
| // Groups without a newest cache can't have been put to disk yet, so |
| // we can synchronously return a reference we have in the working set. |
| if (!group->newest_complete_cache()) |
| return false; |
| |
| // The LoadGroup interface implies also loading the newest cache, so |
| // if loading the newest cache should appear async, so too must the |
| // loading of this group. |
| if (!ShouldCacheLoadAppearAsync(group->newest_complete_cache())) |
| return false; |
| |
| |
| // If any of the old caches are "in use", then the group must also |
| // be memory resident and not require async loading. |
| const AppCacheGroup::Caches& old_caches = group->old_caches(); |
| AppCacheGroup::Caches::const_iterator it = old_caches.begin(); |
| while (it != old_caches.end()) { |
| // "in use" caches don't require async loading |
| if (!ShouldCacheLoadAppearAsync(*it)) |
| return false; |
| ++it; |
| } |
| |
| return true; |
| } |
| |
| bool MockAppCacheStorage::ShouldCacheLoadAppearAsync(const AppCache* cache) { |
| if (!cache) |
| return true; |
| |
| // If the 'stored' ref is the only ref, real storage will have to load from |
| // the database. |
| return IsCacheStored(cache) && cache->HasOneRef(); |
| } |
| |
| } // namespace appcache |