// 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& response_ids) { DeleteResponses(manifest_url, response_ids); } void MockAppCacheStorage::DeleteResponses( const GURL& manifest_url, const std::vector& 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::const_iterator it = response_ids.begin(); while (it != response_ids.end()) { doomed_response_ids_.insert(*it); ++it; } } void MockAppCacheStorage::ProcessGetAllInfo( scoped_refptr delegate_ref) { if (delegate_ref->delegate) delegate_ref->delegate->OnAllInfo(simulated_appcache_info_); } void MockAppCacheStorage::ProcessLoadCache( int64 id, scoped_refptr 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 delegate_ref) { scoped_refptr 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 group, scoped_refptr newest_cache, scoped_refptr 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 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 group, scoped_refptr 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