OLD | NEW |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 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 "ios/chrome/browser/reading_list/reading_list_store.h" | 5 #include "ios/chrome/browser/reading_list/reading_list_store.h" |
6 | 6 |
7 #include "base/bind.h" | 7 #include "base/bind.h" |
8 #include "base/files/file_path.h" | 8 #include "base/files/file_path.h" |
9 #include "base/logging.h" | 9 #include "base/logging.h" |
10 #include "base/memory/ptr_util.h" | 10 #include "base/memory/ptr_util.h" |
| 11 #include "components/sync/api/entity_change.h" |
| 12 #include "components/sync/api/metadata_change_list.h" |
| 13 #include "components/sync/api/metadata_batch.h" |
| 14 #include "components/sync/core/simple_metadata_change_list.h" |
| 15 #include "components/sync/core/shared_model_type_processor.h" |
| 16 #include "components/sync/protocol/model_type_state.pb.h" |
11 #include "ios/chrome/browser/reading_list/proto/reading_list.pb.h" | 17 #include "ios/chrome/browser/reading_list/proto/reading_list.pb.h" |
12 #include "ios/chrome/browser/reading_list/reading_list_model_impl.h" | 18 #include "ios/chrome/browser/reading_list/reading_list_model_impl.h" |
13 #include "ios/web/public/web_thread.h" | 19 #include "ios/web/public/web_thread.h" |
14 | 20 |
15 ReadingListStore::ReadingListStore(std::unique_ptr<ReadingListDB> database, | 21 ReadingListStore::ReadingListStore(std::unique_ptr<ReadingListDB> database, |
16 const base::FilePath& database_dir) | 22 const base::FilePath& database_dir, |
17 : database_(std::move(database)), | 23 StoreFactoryFunction create_store_callback) |
| 24 : ModelTypeService( |
| 25 base::Bind( |
| 26 &syncer::SharedModelTypeProcessor::CreateAsChangeProcessor), |
| 27 syncer::READING_LIST), |
18 database_loaded_(false), | 28 database_loaded_(false), |
19 pending_transaction_(0), | 29 create_store_callback_(create_store_callback), |
20 weak_ptr_factory_(this) { | 30 pending_transaction_(0) {} |
21 database_->Init("ReadingList", database_dir, | |
22 base::Bind(&ReadingListStore::OnDatabaseInit, | |
23 weak_ptr_factory_.GetWeakPtr())); | |
24 } | |
25 | 31 |
26 ReadingListStore::~ReadingListStore() { | 32 ReadingListStore::~ReadingListStore() { |
27 DCHECK(pending_transaction_ == 0); | 33 DCHECK(pending_transaction_ == 0); |
28 } | 34 } |
29 | 35 |
30 void ReadingListStore::OnDatabaseInit(bool success) { | |
31 DCHECK_CURRENTLY_ON(web::WebThread::UI); | |
32 if (!success) { | |
33 database_.reset(); | |
34 } | |
35 } | |
36 | |
37 void ReadingListStore::SetReadingListModel(ReadingListModelImpl* model) { | 36 void ReadingListStore::SetReadingListModel(ReadingListModelImpl* model) { |
38 DCHECK_CURRENTLY_ON(web::WebThread::UI); | 37 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
39 model_ = model; | 38 model_ = model; |
| 39 create_store_callback_.Run( |
| 40 base::Bind(&ReadingListStore::OnStoreCreated, base::AsWeakPtr(this))); |
40 } | 41 } |
41 | 42 |
42 void ReadingListStore::LoadPersistentLists() { | 43 void ReadingListStore::LoadPersistentLists() { |
43 DCHECK_CURRENTLY_ON(web::WebThread::UI); | 44 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
44 DCHECK(model_); | 45 DCHECK(model_); |
45 database_->LoadEntries(base::Bind(&ReadingListStore::OnDatabaseLoad, | 46 // database_->LoadEntries( |
46 weak_ptr_factory_.GetWeakPtr())); | 47 // base::Bind(&ReadingListStore::OnDatabaseLoad, base::AsWeakPtr(this))); |
47 } | 48 } |
48 | 49 |
49 void ReadingListStore::BeginTransaction() { | 50 void ReadingListStore::BeginTransaction() { |
50 DCHECK_CURRENTLY_ON(web::WebThread::UI); | 51 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
51 pending_transaction_++; | 52 pending_transaction_++; |
52 if (pending_transaction_ == 1) { | 53 if (pending_transaction_ == 1) { |
53 pending_keys_to_save_ = base::MakeUnique<ReadingListDB::KeyEntryVector>(); | 54 pending_keys_to_save_ = base::MakeUnique<ReadingListDB::KeyEntryVector>(); |
54 pending_keys_to_remove_ = base::MakeUnique<std::vector<std::string>>(); | 55 pending_keys_to_remove_ = base::MakeUnique<std::vector<std::string>>(); |
55 } | 56 } |
56 } | 57 } |
57 | 58 |
58 void ReadingListStore::CommitTransaction() { | 59 void ReadingListStore::CommitTransaction() { |
59 DCHECK_CURRENTLY_ON(web::WebThread::UI); | 60 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
60 pending_transaction_--; | 61 pending_transaction_--; |
61 if (pending_transaction_ == 0) { | 62 if (pending_transaction_ == 0) { |
62 database_->UpdateEntries(std::move(pending_keys_to_save_), | 63 // database_->UpdateEntries( |
63 std::move(pending_keys_to_remove_), | 64 // std::move(pending_keys_to_save_), |
64 base::Bind(&ReadingListStore::OnDatabaseSave, | 65 // std::move(pending_keys_to_remove_), |
65 weak_ptr_factory_.GetWeakPtr())); | 66 // base::Bind(&ReadingListStore::OnDatabaseSave, |
| 67 // base::AsWeakPtr(this))); |
66 pending_keys_to_save_ = nullptr; | 68 pending_keys_to_save_ = nullptr; |
67 pending_keys_to_remove_ = nullptr; | 69 pending_keys_to_remove_ = nullptr; |
68 } | 70 } |
69 } | 71 } |
70 | 72 |
71 void ReadingListStore::SaveEntry(const ReadingListEntry& entry, bool read) { | 73 void ReadingListStore::SaveEntry(const ReadingListEntry& entry, bool read) { |
72 DCHECK_CURRENTLY_ON(web::WebThread::UI); | 74 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
73 BeginTransaction(); | 75 // BeginTransaction(); |
74 | 76 // |
75 std::unique_ptr<reading_list::ReadingListLocal> pb_entry = | 77 std::unique_ptr<reading_list::ReadingListLocal> pb_entry = |
76 entry.AsReadingListLocal(read); | 78 entry.AsReadingListLocal(read); |
77 // Unref the URL before making asynchronous call. | 79 // Unref the URL before making asynchronous call. |
78 std::string local_key = entry.URL().spec(); | 80 std::string local_key = entry.URL().spec(); |
79 pending_keys_to_save_->push_back(std::make_pair(local_key, *pb_entry)); | |
80 | 81 |
81 CommitTransaction(); | 82 std::unique_ptr<syncer::ModelTypeStore::WriteBatch> batch = |
| 83 store_->CreateWriteBatch(); |
| 84 store_->WriteData(batch.get(), local_key, pb_entry->SerializeAsString()); |
| 85 |
| 86 if (!change_processor()) { |
| 87 store_->CommitWriteBatch( |
| 88 std::move(batch), |
| 89 base::Bind(&ReadingListStore::OnDatabaseSave, base::AsWeakPtr(this))); |
| 90 return; |
| 91 } |
| 92 |
| 93 std::unique_ptr<syncer::MetadataChangeList> metadata_change_list = |
| 94 CreateMetadataChangeList(); |
| 95 |
| 96 std::unique_ptr<syncer::EntityData> entity_data(new syncer::EntityData()); |
| 97 *entity_data->specifics.mutable_reading_list() = pb_entry->entry(); |
| 98 entity_data->non_unique_name = pb_entry->entry().url(); |
| 99 |
| 100 if (read) { |
| 101 change_processor()->Delete(pb_entry->entry().url(), |
| 102 metadata_change_list.get()); |
| 103 } else { |
| 104 change_processor()->Put(pb_entry->entry().url(), std::move(entity_data), |
| 105 metadata_change_list.get()); |
| 106 } |
| 107 |
| 108 static_cast<syncer::SimpleMetadataChangeList*>(metadata_change_list.get()) |
| 109 ->TransferChanges(store_.get(), batch.get()); |
| 110 store_->CommitWriteBatch( |
| 111 std::move(batch), |
| 112 base::Bind(&ReadingListStore::OnDatabaseSave, base::AsWeakPtr(this))); |
82 } | 113 } |
83 | 114 |
84 void ReadingListStore::RemoveEntry(const ReadingListEntry& entry) { | 115 void ReadingListStore::RemoveEntry(const ReadingListEntry& entry) { |
85 DCHECK_CURRENTLY_ON(web::WebThread::UI); | 116 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
86 BeginTransaction(); | 117 std::string local_key = entry.URL().spec(); |
87 pending_keys_to_remove_->push_back(entry.URL().spec()); | 118 std::unique_ptr<syncer::ModelTypeStore::WriteBatch> batch = |
88 CommitTransaction(); | 119 store_->CreateWriteBatch(); |
| 120 store_->DeleteData(batch.get(), local_key); |
| 121 store_->CommitWriteBatch( |
| 122 std::move(batch), |
| 123 base::Bind(&ReadingListStore::OnDatabaseSave, base::AsWeakPtr(this))); |
89 } | 124 } |
90 | 125 |
91 void ReadingListStore::OnDatabaseLoad(bool success, | 126 void ReadingListStore::OnDatabaseLoad( |
92 std::unique_ptr<EntryVector> entries) { | 127 syncer::ModelTypeStore::Result result, |
| 128 std::unique_ptr<syncer::ModelTypeStore::RecordList> entries) { |
93 DCHECK_CURRENTLY_ON(web::WebThread::UI); | 129 DCHECK_CURRENTLY_ON(web::WebThread::UI); |
94 if (!success) { | 130 if (result != syncer::ModelTypeStore::Result::SUCCESS) { |
95 database_.reset(); | |
96 return; | 131 return; |
97 } | 132 } |
98 database_loaded_ = true; | 133 database_loaded_ = true; |
99 auto read = base::MakeUnique<ReadingListEntries>(); | 134 auto read = base::MakeUnique<ReadingListEntries>(); |
100 auto unread = base::MakeUnique<ReadingListEntries>(); | 135 auto unread = base::MakeUnique<ReadingListEntries>(); |
101 | 136 |
102 for (const reading_list::ReadingListLocal& pb_entry : *entries) { | 137 for (const syncer::ModelTypeStore::Record& r : *entries.get()) { |
| 138 // for (const reading_list::ReadingListLocal& pb_entry : *entries) { |
| 139 std::unique_ptr<reading_list::ReadingListLocal> proto = |
| 140 base::MakeUnique<reading_list::ReadingListLocal>(); |
| 141 if (!proto->ParseFromString(r.value)) { |
| 142 continue; |
| 143 // TODO(skym, crbug.com/582460): Handle unrecoverable initialization |
| 144 // failure. |
| 145 } |
| 146 |
103 std::unique_ptr<ReadingListEntry> entry( | 147 std::unique_ptr<ReadingListEntry> entry( |
104 ReadingListEntry::FromReadingListLocal(pb_entry)); | 148 ReadingListEntry::FromReadingListLocal(*proto)); |
105 if (!entry) { | 149 if (!entry) { |
106 continue; | 150 continue; |
107 } | 151 } |
108 if (pb_entry.entry().status() == sync_pb::ReadingListSpecifics::READ) { | 152 if (proto->entry().status() == sync_pb::ReadingListSpecifics::READ) { |
109 read->push_back(std::move(*entry)); | 153 read->push_back(std::move(*entry)); |
110 } else { | 154 } else { |
111 unread->push_back(std::move(*entry)); | 155 unread->push_back(std::move(*entry)); |
112 } | 156 } |
113 } | 157 } |
114 std::sort(read->begin(), read->end(), | 158 std::sort(read->begin(), read->end(), |
115 ReadingListEntry::CompareEntryUpdateTime); | 159 ReadingListEntry::CompareEntryUpdateTime); |
116 std::sort(unread->begin(), unread->end(), | 160 std::sort(unread->begin(), unread->end(), |
117 ReadingListEntry::CompareEntryUpdateTime); | 161 ReadingListEntry::CompareEntryUpdateTime); |
118 | 162 |
119 model_->ModelLoaded(std::move(unread), std::move(read)); | 163 model_->ModelLoaded(std::move(unread), std::move(read)); |
120 } | 164 |
121 | 165 store_->ReadAllMetadata( |
122 void ReadingListStore::OnDatabaseSave(bool success) { | 166 base::Bind(&ReadingListStore::OnReadAllMetadata, base::AsWeakPtr(this))); |
123 DCHECK_CURRENTLY_ON(web::WebThread::UI); | 167 } |
124 if (!success) { | 168 |
125 database_.reset(); | 169 void ReadingListStore::OnReadAllMetadata( |
126 database_loaded_ = false; | 170 syncer::ModelTypeStore::Result result, |
127 } | 171 std::unique_ptr<syncer::ModelTypeStore::RecordList> metadata_records, |
128 } | 172 const std::string& global_metadata) { |
| 173 if (result != syncer::ModelTypeStore::Result::SUCCESS) { |
| 174 // Store has encountered some serious error. We should still be able to |
| 175 // continue as a read only service, since if we got this far we must have |
| 176 // loaded all data out succesfully. |
| 177 return; |
| 178 } |
| 179 |
| 180 // If we have no metadata then we don't want to create a processor. The idea |
| 181 // is that by not having a processor, the services will suffer less of a |
| 182 // performance hit. This isn't terribly applicable for this model type, but |
| 183 // we want this class to be as similar to other services as possible so follow |
| 184 // the convention. |
| 185 if (metadata_records->size() > 0 || !global_metadata.empty()) { |
| 186 CreateChangeProcessor(); |
| 187 } |
| 188 |
| 189 // Set this after OnChangeProcessorSet so that we can correctly avoid giving |
| 190 // the processor empty metadata. We always want to set |has_metadata_loaded_| |
| 191 // at this point so that we'll know to give a processor empty metadata if it |
| 192 // is created later. |
| 193 has_metadata_loaded_ = true; |
| 194 |
| 195 if (!change_processor()) { |
| 196 // This means we haven't been told to start syncing and we don't have any |
| 197 // local metadata. |
| 198 return; |
| 199 } |
| 200 |
| 201 std::unique_ptr<syncer::MetadataBatch> batch(new syncer::MetadataBatch()); |
| 202 sync_pb::ModelTypeState state; |
| 203 if (state.ParseFromString(global_metadata)) { |
| 204 batch->SetModelTypeState(state); |
| 205 } else { |
| 206 // TODO(skym): How bad is this scenario? We may be able to just give an |
| 207 // empty batch to the processor and we'll treat corrupted data type state |
| 208 // as no data type state at all. The question is do we want to add any of |
| 209 // the entity metadata to the batch or completely skip that step? We're |
| 210 // going to have to perform a merge shortly. Does this decision/logic even |
| 211 // belong in this service? |
| 212 change_processor()->OnMetadataLoaded( |
| 213 change_processor()->CreateAndUploadError( |
| 214 FROM_HERE, "Failed to deserialize global metadata."), |
| 215 nullptr); |
| 216 } |
| 217 for (const syncer::ModelTypeStore::Record& r : *metadata_records.get()) { |
| 218 sync_pb::EntityMetadata entity_metadata; |
| 219 if (entity_metadata.ParseFromString(r.value)) { |
| 220 batch->AddMetadata(r.id, entity_metadata); |
| 221 } else { |
| 222 // TODO(skym): This really isn't too bad. We just want to regenerate |
| 223 // metadata for this particular entity. Unfortunately there isn't a |
| 224 // convenient way to tell the processor to do this. |
| 225 LOG(WARNING) << "Failed to deserialize entity metadata."; |
| 226 } |
| 227 } |
| 228 change_processor()->OnMetadataLoaded(syncer::SyncError(), std::move(batch)); |
| 229 } |
| 230 |
| 231 void ReadingListStore::OnDatabaseSave(syncer::ModelTypeStore::Result result) { |
| 232 return; |
| 233 } |
| 234 |
| 235 void ReadingListStore::OnStoreCreated( |
| 236 syncer::ModelTypeStore::Result result, |
| 237 std::unique_ptr<syncer::ModelTypeStore> store) { |
| 238 store_ = std::move(store); |
| 239 store_->ReadAllData( |
| 240 base::Bind(&ReadingListStore::OnDatabaseLoad, base::AsWeakPtr(this))); |
| 241 return; |
| 242 } |
| 243 |
| 244 syncer::ModelTypeService* ReadingListStore::GetModelTypeService() { |
| 245 return this; |
| 246 } |
| 247 |
| 248 // Creates an object used to communicate changes in the sync metadata to the |
| 249 // model type store. |
| 250 std::unique_ptr<syncer::MetadataChangeList> |
| 251 ReadingListStore::CreateMetadataChangeList() { |
| 252 return base::MakeUnique<syncer::SimpleMetadataChangeList>(); |
| 253 } |
| 254 |
| 255 // Perform the initial merge between local and sync data. This should only be |
| 256 // called when a data type is first enabled to start syncing, and there is no |
| 257 // sync metadata. Best effort should be made to match local and sync data. The |
| 258 // keys in the |entity_data_map| will have been created via GetClientTag(...), |
| 259 // and if a local and sync data should match/merge but disagree on tags, the |
| 260 // service should use the sync data's tag. Any local pieces of data that are |
| 261 // not present in sync should immediately be Put(...) to the processor before |
| 262 // returning. The same MetadataChangeList that was passed into this function |
| 263 // can be passed to Put(...) calls. Delete(...) can also be called but should |
| 264 // not be needed for most model types. Durable storage writes, if not able to |
| 265 // combine all change atomically, should save the metadata after the data |
| 266 // changes, so that this merge will be re-driven by sync if is not completely |
| 267 // saved during the current run. |
| 268 syncer::SyncError ReadingListStore::MergeSyncData( |
| 269 std::unique_ptr<syncer::MetadataChangeList> metadata_change_list, |
| 270 syncer::EntityDataMap entity_data_map) { |
| 271 // std::unique_ptr<syncer::ModelTypeStore::WriteBatch> batch = |
| 272 // store_->CreateWriteBatch(); |
| 273 for (const auto& kv : entity_data_map) { |
| 274 const sync_pb::ReadingListSpecifics& specifics = |
| 275 kv.second.value().specifics.reading_list(); |
| 276 |
| 277 if (!model_->CallbackEntryURL( |
| 278 GURL(specifics.url()), |
| 279 base::Bind(&ReadingListStore::NoopEntry, base::AsWeakPtr(this)))) { |
| 280 model_->AddEntry(GURL(specifics.url()), specifics.title()); |
| 281 } |
| 282 |
| 283 // std::unique_ptr<reading_list::ReadingListLocal> proto = |
| 284 // base::MakeUnique<reading_list::ReadingListLocal>(); |
| 285 // |
| 286 // proto->set_allocated_entry(new |
| 287 // sync_pb::ReadingListSpecifics(specifics)); |
| 288 // |
| 289 // // Unref the URL before making asynchronous call. |
| 290 // std::string local_key = specifics.url(); |
| 291 // |
| 292 // |
| 293 // store_->WriteData(batch.get(), local_key, proto->SerializeAsString()); |
| 294 } |
| 295 |
| 296 // store_->CommitWriteBatch( |
| 297 // std::move(batch), |
| 298 // base::Bind(&ReadingListStore::OnDatabaseSave, |
| 299 // base::AsWeakPtr(this))); |
| 300 return syncer::SyncError(); |
| 301 } |
| 302 |
| 303 // Apply changes from the sync server locally. |
| 304 // Please note that |entity_changes| might have fewer entries than |
| 305 // |metadata_change_list| in case when some of the data changes are filtered |
| 306 // out, or even be empty in case when a commit confirmation is processed and |
| 307 // only the metadata needs to persisted. |
| 308 syncer::SyncError ReadingListStore::ApplySyncChanges( |
| 309 std::unique_ptr<syncer::MetadataChangeList> metadata_change_list, |
| 310 syncer::EntityChangeList entity_changes) { |
| 311 // std::unique_ptr<syncer::ModelTypeStore::WriteBatch> batch = |
| 312 // store_->CreateWriteBatch(); |
| 313 for (syncer::EntityChange& change : entity_changes) { |
| 314 if (change.type() == syncer::EntityChange::ACTION_DELETE) { |
| 315 // if (model_->CallbackEntryURL(GURL(change.storage_key()), |
| 316 // base::Bind(&ReadingListStore::NoopEnt
ry, |
| 317 // base::AsWeakPtr(this)))) { |
| 318 // model_->RemoveEntryByUrl(GURL(specifics.url())); |
| 319 // } |
| 320 continue; |
| 321 } else { |
| 322 const sync_pb::ReadingListSpecifics& specifics = |
| 323 change.data().specifics.reading_list(); |
| 324 |
| 325 if (!model_->CallbackEntryURL(GURL(specifics.url()), |
| 326 base::Bind(&ReadingListStore::NoopEntry, |
| 327 base::AsWeakPtr(this)))) { |
| 328 model_->AddEntry(GURL(specifics.url()), specifics.title()); |
| 329 } |
| 330 |
| 331 // proto->set_allocated_entry(new |
| 332 // sync_pb::ReadingListSpecifics(specifics)); |
| 333 // |
| 334 // // Unref the URL before making asynchronous call. |
| 335 // std::string local_key = specifics.url(); |
| 336 // |
| 337 // |
| 338 // store_->WriteData(batch.get(), local_key, |
| 339 // proto->SerializeAsString()); |
| 340 } |
| 341 } |
| 342 // store_->CommitWriteBatch( |
| 343 // std::move(batch), |
| 344 // base::Bind(&ReadingListStore::OnDatabaseSave, |
| 345 // base::AsWeakPtr(this))); |
| 346 |
| 347 return syncer::SyncError(); |
| 348 } |
| 349 |
| 350 void ReadingListStore::NoopEntry(const ReadingListEntry&) {} |
| 351 |
| 352 // Asynchronously retrieve the corresponding sync data for |storage_keys|. |
| 353 void ReadingListStore::GetData(StorageKeyList storage_keys, |
| 354 DataCallback callback) { |
| 355 return; |
| 356 } |
| 357 |
| 358 // Asynchronously retrieve all of the local sync data. |
| 359 void ReadingListStore::GetAllData(DataCallback callback) {} |
| 360 |
| 361 // Get or generate a client tag for |entity_data|. This must be the same tag |
| 362 // that was/would have been generated in the SyncableService/Directory world |
| 363 // for backward compatibility with pre-USS clients. The only time this |
| 364 // theoretically needs to be called is on the creation of local data, however |
| 365 // it is also used to verify the hash of remote data. If a data type was never |
| 366 // launched pre-USS, then method does not need to be different from |
| 367 // GetStorageKey(). |
| 368 std::string ReadingListStore::GetClientTag( |
| 369 const syncer::EntityData& entity_data) { |
| 370 return entity_data.specifics.reading_list().url(); |
| 371 } |
| 372 |
| 373 // Get or generate a storage key for |entity_data|. This will only ever be |
| 374 // called once when first encountering a remote entity. Local changes will |
| 375 // provide their storage keys directly to Put instead of using this method. |
| 376 // Theoretically this function doesn't need to be stable across multiple calls |
| 377 // on the same or different clients, but to keep things simple, it probably |
| 378 // should be. |
| 379 std::string ReadingListStore::GetStorageKey( |
| 380 const syncer::EntityData& entity_data) { |
| 381 return entity_data.specifics.reading_list().url(); |
| 382 } |
| 383 |
| 384 // Overridable notification for when the processor is set. This is typically |
| 385 // when the service should start loading metadata and then subsequently giving |
| 386 // it to the processor. |
| 387 void ReadingListStore::OnChangeProcessorSet() { |
| 388 if (has_metadata_loaded_) { |
| 389 change_processor()->OnMetadataLoaded( |
| 390 syncer::SyncError(), base::MakeUnique<syncer::MetadataBatch>()); |
| 391 } |
| 392 } |
OLD | NEW |