1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "webkit/browser/appcache/mock_appcache_storage.h" 6 7 #include "base/bind.h" 8 #include "base/logging.h" 9 #include "base/memory/ref_counted.h" 10 #include "base/message_loop/message_loop.h" 11 #include "base/stl_util.h" 12 #include "webkit/browser/appcache/appcache.h" 13 #include "webkit/browser/appcache/appcache_entry.h" 14 #include "webkit/browser/appcache/appcache_group.h" 15 #include "webkit/browser/appcache/appcache_response.h" 16 #include "webkit/browser/appcache/appcache_service.h" 17 18 // This is a quick and easy 'mock' implementation of the storage interface 19 // that doesn't put anything to disk. 20 // 21 // We simply add an extra reference to objects when they're put in storage, 22 // and remove the extra reference when they are removed from storage. 23 // Responses are never really removed from the in-memory disk cache. 24 // Delegate callbacks are made asyncly to appropiately mimic what will 25 // happen with a real disk-backed storage impl that involves IO on a 26 // background thread. 27 28 namespace appcache { 29 30 MockAppCacheStorage::MockAppCacheStorage(AppCacheService* service) 31 : AppCacheStorage(service), 32 weak_factory_(this), 33 simulate_make_group_obsolete_failure_(false), 34 simulate_store_group_and_newest_cache_failure_(false), 35 simulate_find_main_resource_(false), 36 simulate_find_sub_resource_(false), 37 simulated_found_cache_id_(kNoCacheId), 38 simulated_found_group_id_(0), 39 simulated_found_network_namespace_(false) { 40 last_cache_id_ = 0; 41 last_group_id_ = 0; 42 last_response_id_ = 0; 43 } 44 45 MockAppCacheStorage::~MockAppCacheStorage() { 46 } 47 48 void MockAppCacheStorage::GetAllInfo(Delegate* delegate) { 49 ScheduleTask( 50 base::Bind(&MockAppCacheStorage::ProcessGetAllInfo, 51 weak_factory_.GetWeakPtr(), 52 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 53 } 54 55 void MockAppCacheStorage::LoadCache(int64 id, Delegate* delegate) { 56 DCHECK(delegate); 57 AppCache* cache = working_set_.GetCache(id); 58 if (ShouldCacheLoadAppearAsync(cache)) { 59 ScheduleTask( 60 base::Bind(&MockAppCacheStorage::ProcessLoadCache, 61 weak_factory_.GetWeakPtr(), id, 62 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 63 return; 64 } 65 ProcessLoadCache(id, GetOrCreateDelegateReference(delegate)); 66 } 67 68 void MockAppCacheStorage::LoadOrCreateGroup( 69 const GURL& manifest_url, Delegate* delegate) { 70 DCHECK(delegate); 71 AppCacheGroup* group = working_set_.GetGroup(manifest_url); 72 if (ShouldGroupLoadAppearAsync(group)) { 73 ScheduleTask( 74 base::Bind(&MockAppCacheStorage::ProcessLoadOrCreateGroup, 75 weak_factory_.GetWeakPtr(), manifest_url, 76 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 77 return; 78 } 79 ProcessLoadOrCreateGroup( 80 manifest_url, GetOrCreateDelegateReference(delegate)); 81 } 82 83 void MockAppCacheStorage::StoreGroupAndNewestCache( 84 AppCacheGroup* group, AppCache* newest_cache, Delegate* delegate) { 85 DCHECK(group && delegate && newest_cache); 86 87 // Always make this operation look async. 88 ScheduleTask( 89 base::Bind(&MockAppCacheStorage::ProcessStoreGroupAndNewestCache, 90 weak_factory_.GetWeakPtr(), make_scoped_refptr(group), 91 make_scoped_refptr(newest_cache), 92 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 93 } 94 95 void MockAppCacheStorage::FindResponseForMainRequest( 96 const GURL& url, const GURL& preferred_manifest_url, Delegate* delegate) { 97 DCHECK(delegate); 98 99 // Note: MockAppCacheStorage does not respect the preferred_manifest_url. 100 101 // Always make this operation look async. 102 ScheduleTask( 103 base::Bind(&MockAppCacheStorage::ProcessFindResponseForMainRequest, 104 weak_factory_.GetWeakPtr(), url, 105 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 106 } 107 108 void MockAppCacheStorage::FindResponseForSubRequest( 109 AppCache* cache, const GURL& url, 110 AppCacheEntry* found_entry, AppCacheEntry* found_fallback_entry, 111 bool* found_network_namespace) { 112 DCHECK(cache && cache->is_complete()); 113 114 // This layer of indirection is here to facilitate testing. 115 if (simulate_find_sub_resource_) { 116 *found_entry = simulated_found_entry_; 117 *found_fallback_entry = simulated_found_fallback_entry_; 118 *found_network_namespace = simulated_found_network_namespace_; 119 simulate_find_sub_resource_ = false; 120 return; 121 } 122 123 GURL fallback_namespace_not_used; 124 GURL intercept_namespace_not_used; 125 cache->FindResponseForRequest( 126 url, found_entry, &intercept_namespace_not_used, 127 found_fallback_entry, &fallback_namespace_not_used, 128 found_network_namespace); 129 } 130 131 void MockAppCacheStorage::MarkEntryAsForeign( 132 const GURL& entry_url, int64 cache_id) { 133 AppCache* cache = working_set_.GetCache(cache_id); 134 if (cache) { 135 AppCacheEntry* entry = cache->GetEntry(entry_url); 136 DCHECK(entry); 137 if (entry) 138 entry->add_types(AppCacheEntry::FOREIGN); 139 } 140 } 141 142 void MockAppCacheStorage::MakeGroupObsolete( 143 AppCacheGroup* group, Delegate* delegate) { 144 DCHECK(group && delegate); 145 146 // Always make this method look async. 147 ScheduleTask( 148 base::Bind(&MockAppCacheStorage::ProcessMakeGroupObsolete, 149 weak_factory_.GetWeakPtr(), make_scoped_refptr(group), 150 make_scoped_refptr(GetOrCreateDelegateReference(delegate)))); 151 } 152 153 AppCacheResponseReader* MockAppCacheStorage::CreateResponseReader( 154 const GURL& manifest_url, int64 group_id, int64 response_id) { 155 if (simulated_reader_) 156 return simulated_reader_.release(); 157 return new AppCacheResponseReader(response_id, group_id, disk_cache()); 158 } 159 160 AppCacheResponseWriter* MockAppCacheStorage::CreateResponseWriter( 161 const GURL& manifest_url, int64 group_id) { 162 return new AppCacheResponseWriter(NewResponseId(), group_id, disk_cache()); 163 } 164 165 void MockAppCacheStorage::DoomResponses( 166 const GURL& manifest_url, const std::vector<int64>& response_ids) { 167 DeleteResponses(manifest_url, response_ids); 168 } 169 170 void MockAppCacheStorage::DeleteResponses( 171 const GURL& manifest_url, const std::vector<int64>& response_ids) { 172 // We don't bother with actually removing responses from the disk-cache, 173 // just keep track of which ids have been doomed or deleted 174 std::vector<int64>::const_iterator it = response_ids.begin(); 175 while (it != response_ids.end()) { 176 doomed_response_ids_.insert(*it); 177 ++it; 178 } 179 } 180 181 void MockAppCacheStorage::ProcessGetAllInfo( 182 scoped_refptr<DelegateReference> delegate_ref) { 183 if (delegate_ref->delegate) 184 delegate_ref->delegate->OnAllInfo(simulated_appcache_info_.get()); 185 } 186 187 void MockAppCacheStorage::ProcessLoadCache( 188 int64 id, scoped_refptr<DelegateReference> delegate_ref) { 189 AppCache* cache = working_set_.GetCache(id); 190 if (delegate_ref->delegate) 191 delegate_ref->delegate->OnCacheLoaded(cache, id); 192 } 193 194 void MockAppCacheStorage::ProcessLoadOrCreateGroup( 195 const GURL& manifest_url, scoped_refptr<DelegateReference> delegate_ref) { 196 scoped_refptr<AppCacheGroup> group(working_set_.GetGroup(manifest_url)); 197 198 // Newly created groups are not put in the stored_groups collection 199 // until StoreGroupAndNewestCache is called. 200 if (!group.get()) 201 group = new AppCacheGroup(service_->storage(), manifest_url, NewGroupId()); 202 203 if (delegate_ref->delegate) 204 delegate_ref->delegate->OnGroupLoaded(group.get(), manifest_url); 205 } 206 207 void MockAppCacheStorage::ProcessStoreGroupAndNewestCache( 208 scoped_refptr<AppCacheGroup> group, 209 scoped_refptr<AppCache> newest_cache, 210 scoped_refptr<DelegateReference> delegate_ref) { 211 Delegate* delegate = delegate_ref->delegate; 212 if (simulate_store_group_and_newest_cache_failure_) { 213 if (delegate) 214 delegate->OnGroupAndNewestCacheStored( 215 group.get(), newest_cache.get(), false, false); 216 return; 217 } 218 219 AddStoredGroup(group.get()); 220 if (newest_cache.get() != group->newest_complete_cache()) { 221 newest_cache->set_complete(true); 222 group->AddCache(newest_cache.get()); 223 AddStoredCache(newest_cache.get()); 224 225 // Copy the collection prior to removal, on final release 226 // of a cache the group's collection will change. 227 AppCacheGroup::Caches copy = group->old_caches(); 228 RemoveStoredCaches(copy); 229 } 230 231 if (delegate) 232 delegate->OnGroupAndNewestCacheStored( 233 group.get(), newest_cache.get(), true, false); 234 } 235 236 namespace { 237 238 struct FoundCandidate { 239 GURL namespace_entry_url; 240 AppCacheEntry entry; 241 int64 cache_id; 242 int64 group_id; 243 GURL manifest_url; 244 bool is_cache_in_use; 245 246 FoundCandidate() 247 : cache_id(kNoCacheId), group_id(0), is_cache_in_use(false) {} 248 }; 249 250 void MaybeTakeNewNamespaceEntry( 251 NamespaceType namespace_type, 252 const AppCacheEntry &entry, 253 const GURL& namespace_url, 254 bool cache_is_in_use, 255 FoundCandidate* best_candidate, 256 GURL* best_candidate_namespace, 257 AppCache* cache, 258 AppCacheGroup* group) { 259 DCHECK(entry.has_response_id()); 260 261 bool take_new_entry = true; 262 263 // Does the new candidate entry trump our current best candidate? 264 if (best_candidate->entry.has_response_id()) { 265 // Longer namespace prefix matches win. 266 size_t candidate_length = 267 namespace_url.spec().length(); 268 size_t best_length = 269 best_candidate_namespace->spec().length(); 270 271 if (candidate_length > best_length) { 272 take_new_entry = true; 273 } else if (candidate_length == best_length && 274 cache_is_in_use && !best_candidate->is_cache_in_use) { 275 take_new_entry = true; 276 } else { 277 take_new_entry = false; 278 } 279 } 280 281 if (take_new_entry) { 282 if (namespace_type == FALLBACK_NAMESPACE) { 283 best_candidate->namespace_entry_url = 284 cache->GetFallbackEntryUrl(namespace_url); 285 } else { 286 best_candidate->namespace_entry_url = 287 cache->GetInterceptEntryUrl(namespace_url); 288 } 289 best_candidate->entry = entry; 290 best_candidate->cache_id = cache->cache_id(); 291 best_candidate->group_id = group->group_id(); 292 best_candidate->manifest_url = group->manifest_url(); 293 best_candidate->is_cache_in_use = cache_is_in_use; 294 *best_candidate_namespace = namespace_url; 295 } 296 } 297 } // namespace 298 299 void MockAppCacheStorage::ProcessFindResponseForMainRequest( 300 const GURL& url, scoped_refptr<DelegateReference> delegate_ref) { 301 if (simulate_find_main_resource_) { 302 simulate_find_main_resource_ = false; 303 if (delegate_ref->delegate) { 304 delegate_ref->delegate->OnMainResponseFound( 305 url, simulated_found_entry_, 306 simulated_found_fallback_url_, simulated_found_fallback_entry_, 307 simulated_found_cache_id_, simulated_found_group_id_, 308 simulated_found_manifest_url_); 309 } 310 return; 311 } 312 313 // This call has no persistent side effects, if the delegate has gone 314 // away, we can just bail out early. 315 if (!delegate_ref->delegate) 316 return; 317 318 // TODO(michaeln): The heuristics around choosing amoungst 319 // multiple candidates is under specified, and just plain 320 // not fully understood. Refine these over time. In particular, 321 // * prefer candidates from newer caches 322 // * take into account the cache associated with the document 323 // that initiated the navigation 324 // * take into account the cache associated with the document 325 // currently residing in the frame being navigated 326 FoundCandidate found_candidate; 327 GURL found_intercept_candidate_namespace; 328 FoundCandidate found_fallback_candidate; 329 GURL found_fallback_candidate_namespace; 330 331 for (StoredGroupMap::const_iterator it = stored_groups_.begin(); 332 it != stored_groups_.end(); ++it) { 333 AppCacheGroup* group = it->second.get(); 334 AppCache* cache = group->newest_complete_cache(); 335 if (group->is_obsolete() || !cache || 336 (url.GetOrigin() != group->manifest_url().GetOrigin())) { 337 continue; 338 } 339 340 AppCacheEntry found_entry; 341 AppCacheEntry found_fallback_entry; 342 GURL found_intercept_namespace; 343 GURL found_fallback_namespace; 344 bool ignore_found_network_namespace = false; 345 bool found = cache->FindResponseForRequest( 346 url, &found_entry, &found_intercept_namespace, 347 &found_fallback_entry, &found_fallback_namespace, 348 &ignore_found_network_namespace); 349 350 // 6.11.1 Navigating across documents, Step 10. 351 // Network namespacing doesn't apply to main resource loads, 352 // and foreign entries are excluded. 353 if (!found || ignore_found_network_namespace || 354 (found_entry.has_response_id() && found_entry.IsForeign()) || 355 (found_fallback_entry.has_response_id() && 356 found_fallback_entry.IsForeign())) { 357 continue; 358 } 359 360 // We have a bias for hits from caches that are in use. 361 bool is_in_use = IsCacheStored(cache) && !cache->HasOneRef(); 362 363 if (found_entry.has_response_id() && 364 found_intercept_namespace.is_empty()) { 365 found_candidate.namespace_entry_url = GURL(); 366 found_candidate.entry = found_entry; 367 found_candidate.cache_id = cache->cache_id(); 368 found_candidate.group_id = group->group_id(); 369 found_candidate.manifest_url = group->manifest_url(); 370 found_candidate.is_cache_in_use = is_in_use; 371 if (is_in_use) 372 break; // We break out of the loop with this direct hit. 373 } else if (found_entry.has_response_id() && 374 !found_intercept_namespace.is_empty()) { 375 MaybeTakeNewNamespaceEntry( 376 INTERCEPT_NAMESPACE, 377 found_entry, found_intercept_namespace, is_in_use, 378 &found_candidate, &found_intercept_candidate_namespace, 379 cache, group); 380 } else { 381 DCHECK(found_fallback_entry.has_response_id()); 382 MaybeTakeNewNamespaceEntry( 383 FALLBACK_NAMESPACE, 384 found_fallback_entry, found_fallback_namespace, is_in_use, 385 &found_fallback_candidate, &found_fallback_candidate_namespace, 386 cache, group); 387 } 388 } 389 390 // Found a direct hit or an intercept namespace hit. 391 if (found_candidate.entry.has_response_id()) { 392 delegate_ref->delegate->OnMainResponseFound( 393 url, found_candidate.entry, found_candidate.namespace_entry_url, 394 AppCacheEntry(), found_candidate.cache_id, found_candidate.group_id, 395 found_candidate.manifest_url); 396 return; 397 } 398 399 // Found a fallback namespace. 400 if (found_fallback_candidate.entry.has_response_id()) { 401 delegate_ref->delegate->OnMainResponseFound( 402 url, AppCacheEntry(), 403 found_fallback_candidate.namespace_entry_url, 404 found_fallback_candidate.entry, 405 found_fallback_candidate.cache_id, 406 found_fallback_candidate.group_id, 407 found_fallback_candidate.manifest_url); 408 return; 409 } 410 411 // Didn't find anything. 412 delegate_ref->delegate->OnMainResponseFound( 413 url, AppCacheEntry(), GURL(), AppCacheEntry(), kNoCacheId, 0, GURL()); 414 } 415 416 void MockAppCacheStorage::ProcessMakeGroupObsolete( 417 scoped_refptr<AppCacheGroup> group, 418 scoped_refptr<DelegateReference> delegate_ref) { 419 if (simulate_make_group_obsolete_failure_) { 420 if (delegate_ref->delegate) 421 delegate_ref->delegate->OnGroupMadeObsolete(group.get(), false); 422 return; 423 } 424 425 RemoveStoredGroup(group.get()); 426 if (group->newest_complete_cache()) 427 RemoveStoredCache(group->newest_complete_cache()); 428 429 // Copy the collection prior to removal, on final release 430 // of a cache the group's collection will change. 431 AppCacheGroup::Caches copy = group->old_caches(); 432 RemoveStoredCaches(copy); 433 434 group->set_obsolete(true); 435 436 // Also remove from the working set, caches for an 'obsolete' group 437 // may linger in use, but the group itself cannot be looked up by 438 // 'manifest_url' in the working set any longer. 439 working_set()->RemoveGroup(group.get()); 440 441 if (delegate_ref->delegate) 442 delegate_ref->delegate->OnGroupMadeObsolete(group.get(), true); 443 } 444 445 void MockAppCacheStorage::ScheduleTask(const base::Closure& task) { 446 pending_tasks_.push_back(task); 447 base::MessageLoop::current()->PostTask( 448 FROM_HERE, 449 base::Bind(&MockAppCacheStorage::RunOnePendingTask, 450 weak_factory_.GetWeakPtr())); 451 } 452 453 void MockAppCacheStorage::RunOnePendingTask() { 454 DCHECK(!pending_tasks_.empty()); 455 base::Closure task = pending_tasks_.front(); 456 pending_tasks_.pop_front(); 457 task.Run(); 458 } 459 460 void MockAppCacheStorage::AddStoredCache(AppCache* cache) { 461 int64 cache_id = cache->cache_id(); 462 if (stored_caches_.find(cache_id) == stored_caches_.end()) { 463 stored_caches_.insert( 464 StoredCacheMap::value_type(cache_id, make_scoped_refptr(cache))); 465 } 466 } 467 468 void MockAppCacheStorage::RemoveStoredCache(AppCache* cache) { 469 // Do not remove from the working set, active caches are still usable 470 // and may be looked up by id until they fall out of use. 471 stored_caches_.erase(cache->cache_id()); 472 } 473 474 void MockAppCacheStorage::RemoveStoredCaches( 475 const AppCacheGroup::Caches& caches) { 476 AppCacheGroup::Caches::const_iterator it = caches.begin(); 477 while (it != caches.end()) { 478 RemoveStoredCache(*it); 479 ++it; 480 } 481 } 482 483 void MockAppCacheStorage::AddStoredGroup(AppCacheGroup* group) { 484 const GURL& url = group->manifest_url(); 485 if (stored_groups_.find(url) == stored_groups_.end()) { 486 stored_groups_.insert( 487 StoredGroupMap::value_type(url, make_scoped_refptr(group))); 488 } 489 } 490 491 void MockAppCacheStorage::RemoveStoredGroup(AppCacheGroup* group) { 492 stored_groups_.erase(group->manifest_url()); 493 } 494 495 bool MockAppCacheStorage::ShouldGroupLoadAppearAsync( 496 const AppCacheGroup* group) { 497 // We'll have to query the database to see if a group for the 498 // manifest_url exists on disk. So return true for async. 499 if (!group) 500 return true; 501 502 // Groups without a newest cache can't have been put to disk yet, so 503 // we can synchronously return a reference we have in the working set. 504 if (!group->newest_complete_cache()) 505 return false; 506 507 // The LoadGroup interface implies also loading the newest cache, so 508 // if loading the newest cache should appear async, so too must the 509 // loading of this group. 510 if (!ShouldCacheLoadAppearAsync(group->newest_complete_cache())) 511 return false; 512 513 514 // If any of the old caches are "in use", then the group must also 515 // be memory resident and not require async loading. 516 const AppCacheGroup::Caches& old_caches = group->old_caches(); 517 AppCacheGroup::Caches::const_iterator it = old_caches.begin(); 518 while (it != old_caches.end()) { 519 // "in use" caches don't require async loading 520 if (!ShouldCacheLoadAppearAsync(*it)) 521 return false; 522 ++it; 523 } 524 525 return true; 526 } 527 528 bool MockAppCacheStorage::ShouldCacheLoadAppearAsync(const AppCache* cache) { 529 if (!cache) 530 return true; 531 532 // If the 'stored' ref is the only ref, real storage will have to load from 533 // the database. 534 return IsCacheStored(cache) && cache->HasOneRef(); 535 } 536 537 } // namespace appcache 538