OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/chromeos/gdata/gdata_files.h" | 5 #include "chrome/browser/chromeos/gdata/gdata_files.h" |
6 | 6 |
| 7 #include <leveldb/db.h> |
7 #include <vector> | 8 #include <vector> |
8 | 9 |
9 #include "base/message_loop_proxy.h" | 10 #include "base/message_loop_proxy.h" |
10 #include "base/platform_file.h" | 11 #include "base/platform_file.h" |
| 12 #include "base/string_number_conversions.h" |
11 #include "base/string_util.h" | 13 #include "base/string_util.h" |
12 #include "base/stringprintf.h" | 14 #include "base/stringprintf.h" |
| 15 #include "base/sequenced_task_runner.h" |
13 #include "base/tracked_objects.h" | 16 #include "base/tracked_objects.h" |
14 #include "base/utf_string_conversions.h" | 17 #include "base/utf_string_conversions.h" |
15 #include "chrome/browser/chromeos/gdata/gdata.pb.h" | 18 #include "chrome/browser/chromeos/gdata/gdata.pb.h" |
16 #include "chrome/browser/chromeos/gdata/gdata_util.h" | 19 #include "chrome/browser/chromeos/gdata/gdata_util.h" |
17 #include "chrome/browser/chromeos/gdata/gdata_wapi_parser.h" | 20 #include "chrome/browser/chromeos/gdata/gdata_wapi_parser.h" |
| 21 #include "content/public/browser/browser_thread.h" |
18 #include "net/base/escape.h" | 22 #include "net/base/escape.h" |
19 | 23 |
| 24 using content::BrowserThread; |
| 25 |
20 namespace gdata { | 26 namespace gdata { |
21 namespace { | 27 namespace { |
22 | 28 |
23 const char kSlash[] = "/"; | 29 const char kSlash[] = "/"; |
24 const char kEscapedSlash[] = "\xE2\x88\x95"; | 30 const char kEscapedSlash[] = "\xE2\x88\x95"; |
25 | 31 |
| 32 // m: prefix for filesystem metadata db keys, version and largest_changestamp. |
| 33 // r: prefix for resource id db keys. |
| 34 const char kDBKeyLargestChangestamp[] = "m:largest_changestamp"; |
| 35 const char kDBKeyVersion[] = "m:version"; |
| 36 const char kDBKeyResourceIdPrefix[] = "r:"; |
| 37 |
26 // Extracts resource_id out of edit url. | 38 // Extracts resource_id out of edit url. |
27 std::string ExtractResourceId(const GURL& url) { | 39 std::string ExtractResourceId(const GURL& url) { |
28 return net::UnescapeURLComponent(url.ExtractFileName(), | 40 return net::UnescapeURLComponent(url.ExtractFileName(), |
29 net::UnescapeRule::URL_SPECIAL_CHARS); | 41 net::UnescapeRule::URL_SPECIAL_CHARS); |
30 } | 42 } |
31 | 43 |
32 // Returns true if |proto| is a valid proto as the root directory. | 44 // Returns true if |proto| is a valid proto as the root directory. |
33 // Used to reject incompatible proto. | 45 // Used to reject incompatible proto. |
34 bool IsValidRootDirectoryProto(const GDataDirectoryProto& proto) { | 46 bool IsValidRootDirectoryProto(const GDataDirectoryProto& proto) { |
35 const GDataEntryProto& entry_proto = proto.gdata_entry(); | 47 const GDataEntryProto& entry_proto = proto.gdata_entry(); |
(...skipping 380 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
416 GDataDirectory* dir = iter->second; | 428 GDataDirectory* dir = iter->second; |
417 // Remove directories recursively. | 429 // Remove directories recursively. |
418 dir->RemoveChildren(); | 430 dir->RemoveChildren(); |
419 if (directory_service_) | 431 if (directory_service_) |
420 directory_service_->RemoveEntryFromResourceMap(dir); | 432 directory_service_->RemoveEntryFromResourceMap(dir); |
421 } | 433 } |
422 STLDeleteValues(&child_directories_); | 434 STLDeleteValues(&child_directories_); |
423 child_directories_.clear(); | 435 child_directories_.clear(); |
424 } | 436 } |
425 | 437 |
| 438 // ResourceMetadataDB implementation. |
| 439 |
| 440 // Params for GDatadirectoryServiceDB::Create. |
| 441 struct CreateDBParams { |
| 442 CreateDBParams(const FilePath& db_path, |
| 443 base::SequencedTaskRunner* blocking_task_runner) |
| 444 : db_path(db_path), |
| 445 blocking_task_runner(blocking_task_runner) { |
| 446 } |
| 447 |
| 448 FilePath db_path; |
| 449 scoped_refptr<base::SequencedTaskRunner> blocking_task_runner; |
| 450 scoped_ptr<ResourceMetadataDB> db; |
| 451 GDataDirectoryService::SerializedMap serialized_resources; |
| 452 }; |
| 453 |
| 454 // Wrapper for level db. All methods must be called on blocking thread. |
| 455 class ResourceMetadataDB { |
| 456 public: |
| 457 ResourceMetadataDB(const FilePath& db_path, |
| 458 base::SequencedTaskRunner* blocking_task_runner); |
| 459 |
| 460 // Initializes the database. |
| 461 void Init(); |
| 462 |
| 463 // Reads the database into |serialized_resources|. |
| 464 void Read(GDataDirectoryService::SerializedMap* serialized_resources); |
| 465 |
| 466 // Saves |serialized_resources| to the database. |
| 467 void Save(const GDataDirectoryService::SerializedMap& serialized_resources); |
| 468 |
| 469 private: |
| 470 // Clears the database. |
| 471 void Clear(); |
| 472 |
| 473 scoped_refptr<base::SequencedTaskRunner> blocking_task_runner_; |
| 474 scoped_ptr<leveldb::DB> level_db_; |
| 475 FilePath db_path_; |
| 476 }; |
| 477 |
| 478 ResourceMetadataDB::ResourceMetadataDB(const FilePath& db_path, |
| 479 base::SequencedTaskRunner* blocking_task_runner) |
| 480 : blocking_task_runner_(blocking_task_runner), |
| 481 db_path_(db_path) { |
| 482 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
| 483 } |
| 484 |
| 485 // Creates, initializes and reads from the database. |
| 486 // This must be defined after ResourceMetadataDB and CreateDBParams. |
| 487 static void CreateResourceMetadataDBOnBlockingPool( |
| 488 CreateDBParams* params) { |
| 489 DCHECK(params->blocking_task_runner->RunsTasksOnCurrentThread()); |
| 490 DCHECK(!params->db_path.empty()); |
| 491 |
| 492 params->db.reset(new ResourceMetadataDB(params->db_path, |
| 493 params->blocking_task_runner)); |
| 494 params->db->Init(); |
| 495 params->db->Read(¶ms->serialized_resources); |
| 496 } |
| 497 |
| 498 void ResourceMetadataDB::Init() { |
| 499 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
| 500 DCHECK(!db_path_.empty()); |
| 501 |
| 502 DVLOG(1) << "Init " << db_path_.value(); |
| 503 |
| 504 leveldb::DB* level_db = NULL; |
| 505 leveldb::Options options; |
| 506 options.create_if_missing = true; |
| 507 leveldb::Status db_status = leveldb::DB::Open(options, db_path_.value(), |
| 508 &level_db); |
| 509 DCHECK(level_db); |
| 510 DCHECK(db_status.ok()); |
| 511 level_db_.reset(level_db); |
| 512 } |
| 513 |
| 514 void ResourceMetadataDB::Read( |
| 515 GDataDirectoryService::SerializedMap* serialized_resources) { |
| 516 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
| 517 DCHECK(serialized_resources); |
| 518 DVLOG(1) << "Read " << db_path_.value(); |
| 519 |
| 520 scoped_ptr<leveldb::Iterator> iter(level_db_->NewIterator( |
| 521 leveldb::ReadOptions())); |
| 522 for (iter->SeekToFirst(); iter->Valid(); iter->Next()) { |
| 523 DVLOG(1) << "Read, resource " << iter->key().ToString(); |
| 524 serialized_resources->insert(std::make_pair(iter->key().ToString(), |
| 525 iter->value().ToString())); |
| 526 } |
| 527 } |
| 528 |
| 529 void ResourceMetadataDB::Save( |
| 530 const GDataDirectoryService::SerializedMap& serialized_resources) { |
| 531 DCHECK(blocking_task_runner_->RunsTasksOnCurrentThread()); |
| 532 |
| 533 Clear(); |
| 534 for (GDataDirectoryService::SerializedMap::const_iterator iter = |
| 535 serialized_resources.begin(); |
| 536 iter != serialized_resources.end(); ++iter) { |
| 537 DVLOG(1) << "Saving resource " << iter->first << " to db"; |
| 538 leveldb::Status status = level_db_->Put(leveldb::WriteOptions(), |
| 539 leveldb::Slice(iter->first), |
| 540 leveldb::Slice(iter->second)); |
| 541 if (!status.ok()) { |
| 542 LOG(ERROR) << "leveldb Put failed of " << iter->first |
| 543 << ", with " << status.ToString(); |
| 544 NOTREACHED(); |
| 545 } |
| 546 } |
| 547 } |
| 548 |
| 549 void ResourceMetadataDB::Clear() { |
| 550 level_db_.reset(); |
| 551 leveldb::DestroyDB(db_path_.value(), leveldb::Options()); |
| 552 Init(); |
| 553 } |
| 554 |
426 // GDataDirectoryService class implementation. | 555 // GDataDirectoryService class implementation. |
427 | 556 |
428 GDataDirectoryService::GDataDirectoryService() | 557 GDataDirectoryService::GDataDirectoryService() |
429 : serialized_size_(0), | 558 : blocking_task_runner_(NULL), |
| 559 serialized_size_(0), |
430 largest_changestamp_(0), | 560 largest_changestamp_(0), |
431 origin_(UNINITIALIZED) { | 561 origin_(UNINITIALIZED), |
| 562 weak_ptr_factory_(ALLOW_THIS_IN_INITIALIZER_LIST(this)) { |
432 root_.reset(new GDataDirectory(NULL, this)); | 563 root_.reset(new GDataDirectory(NULL, this)); |
433 root_->set_title(kGDataRootDirectory); | 564 root_->set_title(kGDataRootDirectory); |
434 root_->SetBaseNameFromTitle(); | 565 root_->SetBaseNameFromTitle(); |
435 root_->set_resource_id(kGDataRootDirectoryResourceId); | 566 root_->set_resource_id(kGDataRootDirectoryResourceId); |
436 AddEntryToResourceMap(root_.get()); | 567 AddEntryToResourceMap(root_.get()); |
437 } | 568 } |
438 | 569 |
439 GDataDirectoryService::~GDataDirectoryService() { | 570 GDataDirectoryService::~GDataDirectoryService() { |
| 571 ClearRoot(); |
| 572 |
| 573 // Ensure db is closed on the blocking pool. |
| 574 if (blocking_task_runner_ && directory_service_db_.get()) |
| 575 blocking_task_runner_->DeleteSoon(FROM_HERE, |
| 576 directory_service_db_.release()); |
| 577 } |
| 578 |
| 579 void GDataDirectoryService::ClearRoot() { |
440 // Note that children have a reference to root_, | 580 // Note that children have a reference to root_, |
441 // so we need to delete them here. | 581 // so we need to delete them here. |
442 root_->RemoveChildren(); | 582 root_->RemoveChildren(); |
443 RemoveEntryFromResourceMap(root_.get()); | 583 RemoveEntryFromResourceMap(root_.get()); |
444 DCHECK(resource_map_.empty()); | 584 DCHECK(resource_map_.empty()); |
445 resource_map_.clear(); | 585 resource_map_.clear(); |
| 586 root_.reset(); |
446 } | 587 } |
447 | 588 |
448 void GDataDirectoryService::AddEntryToDirectory( | 589 void GDataDirectoryService::AddEntryToDirectory( |
449 const FilePath& directory_path, | 590 const FilePath& directory_path, |
450 GDataEntry* entry, | 591 GDataEntry* entry, |
451 const FileOperationCallback& callback) { | 592 const FileOperationCallback& callback) { |
452 GDataEntry* destination = FindEntryByPathSync(directory_path); | 593 GDataEntry* destination = FindEntryByPathSync(directory_path); |
453 GDataFileError error = GDATA_FILE_ERROR_FAILED; | 594 GDataFileError error = GDATA_FILE_ERROR_FAILED; |
454 if (!destination) { | 595 if (!destination) { |
455 error = GDATA_FILE_ERROR_NOT_FOUND; | 596 error = GDATA_FILE_ERROR_NOT_FOUND; |
(...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
554 GDataDirectory* entry_parent = old_entry ? old_entry->parent() : NULL; | 695 GDataDirectory* entry_parent = old_entry ? old_entry->parent() : NULL; |
555 if (entry_parent) { | 696 if (entry_parent) { |
556 DCHECK_EQ(fresh_file->resource_id(), old_entry->resource_id()); | 697 DCHECK_EQ(fresh_file->resource_id(), old_entry->resource_id()); |
557 DCHECK(old_entry->AsGDataFile()); | 698 DCHECK(old_entry->AsGDataFile()); |
558 | 699 |
559 entry_parent->RemoveEntry(old_entry); | 700 entry_parent->RemoveEntry(old_entry); |
560 entry_parent->AddEntry(fresh_file.release()); | 701 entry_parent->AddEntry(fresh_file.release()); |
561 } | 702 } |
562 } | 703 } |
563 | 704 |
| 705 void GDataDirectoryService::InitFromDB( |
| 706 const FilePath& db_path, |
| 707 base::SequencedTaskRunner* blocking_task_runner, |
| 708 const FileOperationCallback& callback) { |
| 709 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 710 DCHECK(!db_path.empty()); |
| 711 DCHECK(blocking_task_runner); |
| 712 |
| 713 if (directory_service_db_.get()) { |
| 714 if (!callback.is_null()) |
| 715 callback.Run(GDATA_FILE_ERROR_FAILED); |
| 716 return; |
| 717 } |
| 718 |
| 719 blocking_task_runner_ = blocking_task_runner; |
| 720 |
| 721 DVLOG(1) << "InitFromDB " << db_path.value(); |
| 722 |
| 723 CreateDBParams* create_params = |
| 724 new CreateDBParams(db_path, blocking_task_runner); |
| 725 blocking_task_runner_->PostTaskAndReply( |
| 726 FROM_HERE, |
| 727 base::Bind(&CreateResourceMetadataDBOnBlockingPool, |
| 728 create_params), |
| 729 base::Bind(&GDataDirectoryService::InitResourceMap, |
| 730 weak_ptr_factory_.GetWeakPtr(), |
| 731 base::Owned(create_params), |
| 732 callback)); |
| 733 } |
| 734 |
| 735 void GDataDirectoryService::InitResourceMap( |
| 736 CreateDBParams* create_params, |
| 737 const FileOperationCallback& callback) { |
| 738 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 739 DCHECK(create_params); |
| 740 DCHECK(!directory_service_db_.get()); |
| 741 |
| 742 SerializedMap* serialized_resources = &create_params->serialized_resources; |
| 743 directory_service_db_ = create_params->db.Pass(); |
| 744 if (serialized_resources->empty()) { |
| 745 origin_ = INITIALIZING; |
| 746 if (!callback.is_null()) |
| 747 callback.Run(GDATA_FILE_ERROR_NOT_FOUND); |
| 748 return; |
| 749 } |
| 750 |
| 751 ClearRoot(); |
| 752 |
| 753 // Version check. |
| 754 int32 version = 0; |
| 755 SerializedMap::iterator iter = serialized_resources->find(kDBKeyVersion); |
| 756 if (iter == serialized_resources->end() || |
| 757 !base::StringToInt(iter->second, &version) || |
| 758 version != kProtoVersion) { |
| 759 if (!callback.is_null()) |
| 760 callback.Run(GDATA_FILE_ERROR_FAILED); |
| 761 return; |
| 762 } |
| 763 serialized_resources->erase(iter); |
| 764 |
| 765 // Get the largest changestamp. |
| 766 iter = serialized_resources->find(kDBKeyLargestChangestamp); |
| 767 if (iter == serialized_resources->end() || |
| 768 !base::StringToInt(iter->second, &largest_changestamp_)) { |
| 769 NOTREACHED() << "Could not find/parse largest_changestamp"; |
| 770 if (!callback.is_null()) |
| 771 callback.Run(GDATA_FILE_ERROR_FAILED); |
| 772 return; |
| 773 } else { |
| 774 DVLOG(1) << "InitResourceMap largest_changestamp_" << largest_changestamp_; |
| 775 serialized_resources->erase(iter); |
| 776 } |
| 777 |
| 778 ResourceMap resource_map; |
| 779 for (SerializedMap::const_iterator iter = serialized_resources->begin(); |
| 780 iter != serialized_resources->end(); ++iter) { |
| 781 if (iter->first.find(kDBKeyResourceIdPrefix) != 0) { |
| 782 NOTREACHED() << "Incorrect prefix for db key " << iter->first; |
| 783 continue; |
| 784 } |
| 785 |
| 786 const std::string resource_id = |
| 787 iter->first.substr(strlen(kDBKeyResourceIdPrefix)); |
| 788 scoped_ptr<GDataEntry> entry = FromProtoString(iter->second); |
| 789 if (entry.get()) { |
| 790 DVLOG(1) << "Inserting resource " << resource_id |
| 791 << " into resource_map"; |
| 792 resource_map.insert(std::make_pair(resource_id, entry.release())); |
| 793 } else { |
| 794 NOTREACHED() << "Failed to parse GDataEntry for resource " << resource_id; |
| 795 } |
| 796 } |
| 797 |
| 798 // Fix up parent-child relations. |
| 799 for (ResourceMap::iterator iter = resource_map.begin(); |
| 800 iter != resource_map.end(); ++iter) { |
| 801 GDataEntry* entry = iter->second; |
| 802 ResourceMap::iterator parent_it = |
| 803 resource_map.find(entry->parent_resource_id()); |
| 804 if (parent_it != resource_map.end()) { |
| 805 GDataDirectory* parent = parent_it->second->AsGDataDirectory(); |
| 806 if (parent) { |
| 807 DVLOG(1) << "Adding " << entry->resource_id() |
| 808 << " as a child of " << parent->resource_id(); |
| 809 parent->AddEntry(entry); |
| 810 } else { |
| 811 NOTREACHED() << "Parent is not a directory " << parent->resource_id(); |
| 812 } |
| 813 } else if (entry->resource_id() == kGDataRootDirectoryResourceId) { |
| 814 root_.reset(entry->AsGDataDirectory()); |
| 815 DCHECK(root_.get()); |
| 816 AddEntryToResourceMap(root_.get()); |
| 817 } else { |
| 818 NOTREACHED() << "Missing parent id " << entry->parent_resource_id() |
| 819 << " for resource " << entry->resource_id(); |
| 820 } |
| 821 } |
| 822 |
| 823 DCHECK(root_.get()); |
| 824 DCHECK_EQ(resource_map.size(), resource_map_.size()); |
| 825 DCHECK_EQ(resource_map.size(), serialized_resources->size()); |
| 826 |
| 827 origin_ = FROM_CACHE; |
| 828 |
| 829 if (!callback.is_null()) |
| 830 callback.Run(GDATA_FILE_OK); |
| 831 } |
| 832 |
| 833 void GDataDirectoryService::SaveToDB() { |
| 834 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| 835 |
| 836 if (!blocking_task_runner_ || !directory_service_db_.get()) { |
| 837 NOTREACHED(); |
| 838 return; |
| 839 } |
| 840 |
| 841 size_t serialized_size = 0; |
| 842 SerializedMap serialized_resources; |
| 843 for (ResourceMap::const_iterator iter = resource_map_.begin(); |
| 844 iter != resource_map_.end(); ++iter) { |
| 845 GDataEntryProto proto; |
| 846 iter->second->ToProtoFull(&proto); |
| 847 std::string serialized_string; |
| 848 const bool ok = proto.SerializeToString(&serialized_string); |
| 849 DCHECK(ok); |
| 850 if (ok) { |
| 851 serialized_resources.insert( |
| 852 std::make_pair(std::string(kDBKeyResourceIdPrefix) + iter->first, |
| 853 serialized_string)); |
| 854 serialized_size += serialized_string.size(); |
| 855 } |
| 856 } |
| 857 |
| 858 serialized_resources.insert(std::make_pair(kDBKeyVersion, |
| 859 base::IntToString(kProtoVersion))); |
| 860 serialized_resources.insert(std::make_pair(kDBKeyLargestChangestamp, |
| 861 base::IntToString(largest_changestamp_))); |
| 862 set_last_serialized(base::Time::Now()); |
| 863 set_serialized_size(serialized_size); |
| 864 |
| 865 blocking_task_runner_->PostTask( |
| 866 FROM_HERE, |
| 867 base::Bind(&ResourceMetadataDB::Save, |
| 868 base::Unretained(directory_service_db_.get()), |
| 869 serialized_resources)); |
| 870 } |
| 871 |
564 // Convert to/from proto. | 872 // Convert to/from proto. |
565 | 873 |
566 // static | 874 // static |
567 void GDataEntry::ConvertProtoToPlatformFileInfo( | 875 void GDataEntry::ConvertProtoToPlatformFileInfo( |
568 const PlatformFileInfoProto& proto, | 876 const PlatformFileInfoProto& proto, |
569 base::PlatformFileInfo* file_info) { | 877 base::PlatformFileInfo* file_info) { |
570 file_info->size = proto.size(); | 878 file_info->size = proto.size(); |
571 file_info->is_directory = proto.is_directory(); | 879 file_info->is_directory = proto.is_directory(); |
572 file_info->is_symbolic_link = proto.is_symbolic_link(); | 880 file_info->is_symbolic_link = proto.is_symbolic_link(); |
573 file_info->last_modified = base::Time::FromInternalValue( | 881 file_info->last_modified = base::Time::FromInternalValue( |
(...skipping 187 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
761 | 1069 |
762 if (!root_->FromProto(proto.gdata_directory())) | 1070 if (!root_->FromProto(proto.gdata_directory())) |
763 return false; | 1071 return false; |
764 | 1072 |
765 origin_ = FROM_CACHE; | 1073 origin_ = FROM_CACHE; |
766 largest_changestamp_ = proto.largest_changestamp(); | 1074 largest_changestamp_ = proto.largest_changestamp(); |
767 | 1075 |
768 return true; | 1076 return true; |
769 } | 1077 } |
770 | 1078 |
| 1079 scoped_ptr<GDataEntry> GDataDirectoryService::FromProtoString( |
| 1080 const std::string& serialized_proto) { |
| 1081 GDataEntryProto entry_proto; |
| 1082 if (!entry_proto.ParseFromString(serialized_proto)) |
| 1083 return scoped_ptr<GDataEntry>(); |
| 1084 |
| 1085 scoped_ptr<GDataEntry> entry; |
| 1086 if (entry_proto.file_info().is_directory()) { |
| 1087 entry.reset(new GDataDirectory(NULL, this)); |
| 1088 // Call GDataEntry::FromProto instead of GDataDirectory::FromProto because |
| 1089 // the proto does not include children. |
| 1090 if (!entry->FromProto(entry_proto)) { |
| 1091 NOTREACHED() << "FromProto (directory) failed"; |
| 1092 entry.reset(); |
| 1093 } |
| 1094 } else { |
| 1095 scoped_ptr<GDataFile> file(new GDataFile(NULL, this)); |
| 1096 // Call GDataFile::FromProto. |
| 1097 if (file->FromProto(entry_proto)) { |
| 1098 entry.reset(file.release()); |
| 1099 } else { |
| 1100 NOTREACHED() << "FromProto (file) failed"; |
| 1101 } |
| 1102 } |
| 1103 return entry.Pass(); |
| 1104 } |
| 1105 |
771 } // namespace gdata | 1106 } // namespace gdata |
OLD | NEW |