Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 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 "content/browser/service_worker/service_worker_database.h" | 5 #include "content/browser/service_worker/service_worker_database.h" |
| 6 | 6 |
| 7 #include <string> | 7 #include <string> |
| 8 | 8 |
| 9 #include "base/file_util.h" | 9 #include "base/file_util.h" |
| 10 #include "base/location.h" | 10 #include "base/location.h" |
| 11 #include "base/logging.h" | 11 #include "base/logging.h" |
| 12 #include "base/strings/string_number_conversions.h" | 12 #include "base/strings/string_number_conversions.h" |
| 13 #include "base/strings/string_split.h" | |
| 14 #include "base/strings/string_util.h" | |
| 15 #include "base/strings/stringprintf.h" | |
| 16 #include "content/browser/service_worker/service_worker_database.pb.h" | |
| 13 #include "third_party/leveldatabase/src/helpers/memenv/memenv.h" | 17 #include "third_party/leveldatabase/src/helpers/memenv/memenv.h" |
| 14 #include "third_party/leveldatabase/src/include/leveldb/db.h" | 18 #include "third_party/leveldatabase/src/include/leveldb/db.h" |
| 15 #include "third_party/leveldatabase/src/include/leveldb/env.h" | 19 #include "third_party/leveldatabase/src/include/leveldb/env.h" |
| 16 #include "third_party/leveldatabase/src/include/leveldb/write_batch.h" | 20 #include "third_party/leveldatabase/src/include/leveldb/write_batch.h" |
| 17 | 21 |
| 18 // LevelDB database schema | 22 // LevelDB database schema |
| 19 // ======================= | 23 // ======================= |
| 20 // | 24 // |
| 21 // NOTE | 25 // NOTE |
| 22 // - int64 value is serialized as a string by base::Int64ToString(). | 26 // - int64 value is serialized as a string by base::Int64ToString(). |
| 23 // | 27 // |
| 24 // Version 1 (in sorted order) | 28 // Version 1 (in sorted order) |
| 25 // key: "DB_VERSION" | 29 // key: "INITDATA_DB_VERSION" |
| 26 // value: "1" | 30 // value: "1" |
| 27 // | 31 // |
| 28 // key: "NEXT_REGISTRATION_ID" | 32 // key: "INITDATA_NEXT_REGISTRATION_ID" |
| 29 // value: <int64 'next_available_registration_id'> | 33 // value: <int64 'next_available_registration_id'> |
| 30 // | 34 // |
| 31 // key: "NEXT_RESOURCE_ID" | 35 // key: "INITDATA_NEXT_RESOURCE_ID" |
| 32 // value: <int64 'next_available_resource_id'> | 36 // value: <int64 'next_available_resource_id'> |
| 33 // | 37 // |
| 34 // key: "NEXT_VERSION_ID" | 38 // key: "INITDATA_NEXT_VERSION_ID" |
| 35 // value: <int64 'next_available_version_id'> | 39 // value: <int64 'next_available_version_id'> |
| 40 // | |
| 41 // key: "INITDATA_UNIQUE_ORIGIN:" + <GURL 'origin' serialized by GURL::spec()> | |
| 42 // value: <empty> | |
| 43 // | |
| 44 // key: "REG:" + (1) + '\x00' + (2) | |
| 45 // (1) <GURL 'origin' serialized by GURL::spec()> | |
| 46 // (2) <int64 'registration_id'> | |
| 47 // (ex. "REG:http://example.com\x00123456") | |
| 48 // value: <ServiceWorkerRegistrationData serialized as a string> | |
| 36 | 49 |
| 37 namespace content { | 50 namespace content { |
| 38 | 51 |
| 39 namespace { | 52 namespace { |
| 40 | 53 |
| 41 const char kDatabaseVersionKey[] = "DB_VERSION"; | 54 const char kDatabaseVersionKey[] = "INITDATA_DB_VERSION"; |
| 42 const char kNextRegIdKey[] = "NEXT_REGISTRATION_ID"; | 55 const char kNextRegIdKey[] = "INITDATA_NEXT_REGISTRATION_ID"; |
| 43 const char kNextResIdKey[] = "NEXT_RESOURCE_ID"; | 56 const char kNextResIdKey[] = "INITDATA_NEXT_RESOURCE_ID"; |
| 44 const char kNextVerIdKey[] = "NEXT_VERSION_ID"; | 57 const char kNextVerIdKey[] = "INITDATA_NEXT_VERSION_ID"; |
| 58 const char kUniqueOriginKey[] = "INITDATA_UNIQUE_ORIGIN:"; | |
| 59 | |
| 60 const char kRegKeyPrefix[] = "REG:"; | |
| 61 const char kKeySeparator = '\x00'; | |
| 45 | 62 |
| 46 const int64 kCurrentSchemaVersion = 1; | 63 const int64 kCurrentSchemaVersion = 1; |
| 47 | 64 |
| 48 } // namespace | 65 bool RemovePrefix(const std::string& str, |
| 49 | 66 const std::string& prefix, |
| 50 ServiceWorkerDatabase::RegistrationData::RegistrationData() | 67 std::string* out) { |
| 51 : registration_id(-1), | 68 DCHECK(out); |
| 52 version_id(-1), | 69 if (!StartsWithASCII(str, prefix, true)) |
| 53 is_active(false), | 70 return false; |
| 54 has_fetch_handler(false) { | 71 *out = str.substr(prefix.size()); |
| 72 return true; | |
| 55 } | 73 } |
| 56 | 74 |
| 57 ServiceWorkerDatabase::RegistrationData::~RegistrationData() { | 75 std::string CreateRegistrationKey(int64 registration_id, |
| 76 const GURL& origin) { | |
| 77 return base::StringPrintf("%s%s%c%s", | |
| 78 kRegKeyPrefix, | |
| 79 origin.spec().c_str(), | |
| 80 kKeySeparator, | |
| 81 base::Int64ToString(registration_id).c_str()); | |
| 58 } | 82 } |
| 59 | 83 |
| 84 std::string CreateUniqueOriginKey(const GURL& origin) { | |
| 85 return base::StringPrintf("%s%s", kUniqueOriginKey, origin.spec().c_str()); | |
| 86 } | |
| 87 | |
| 88 void PutRegistrationDataToBatch(const ServiceWorkerRegistrationData& data, | |
| 89 leveldb::WriteBatch* batch) { | |
| 90 DCHECK(batch); | |
| 91 std::string value; | |
| 92 bool success = data.SerializeToString(&value); | |
| 93 DCHECK(success); | |
| 94 GURL origin = GURL(data.scope_url()).GetOrigin(); | |
|
michaeln
2014/04/25 00:48:21
Maybe this class should entirely encapsulate the u
nhiroki
2014/04/25 02:06:11
I see. Will make them as required.
kinuko
2014/04/25 03:43:27
Fyi, it's possible to populate a field with a defa
nhiroki
2014/04/25 11:01:05
And there is base::Time issue.
michaeln
2014/04/26 00:29:02
I think the serialization format is an impl detail
| |
| 95 batch->Put(CreateRegistrationKey(data.registration_id(), origin), value); | |
| 96 } | |
| 97 | |
| 98 void PutUniqueOriginToBatch(const GURL& origin, | |
| 99 leveldb::WriteBatch* batch) { | |
| 100 // Value should be empty. | |
| 101 batch->Put(CreateUniqueOriginKey(origin), ""); | |
| 102 } | |
| 103 | |
| 104 bool ParseRegistrationData(const std::string& serialized, | |
| 105 ServiceWorkerRegistrationData* out) { | |
| 106 DCHECK(out); | |
| 107 ServiceWorkerRegistrationData data; | |
| 108 if (!data.ParseFromString(serialized)) | |
| 109 return false; | |
| 110 if (GURL(data.scope_url()).GetOrigin() != GURL(data.script_url()).GetOrigin()) | |
| 111 return false; | |
| 112 *out = data; | |
| 113 return true; | |
| 114 } | |
| 115 | |
| 116 } // namespace | |
| 117 | |
| 60 ServiceWorkerDatabase::ServiceWorkerDatabase(const base::FilePath& path) | 118 ServiceWorkerDatabase::ServiceWorkerDatabase(const base::FilePath& path) |
| 61 : path_(path), | 119 : path_(path), |
| 62 is_disabled_(false), | 120 is_disabled_(false), |
| 63 was_corruption_detected_(false) { | 121 was_corruption_detected_(false) { |
| 64 } | 122 } |
| 65 | 123 |
| 66 ServiceWorkerDatabase::~ServiceWorkerDatabase() { | 124 ServiceWorkerDatabase::~ServiceWorkerDatabase() { |
| 67 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | 125 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); |
| 68 db_.reset(); | 126 db_.reset(); |
| 69 } | 127 } |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 88 !ReadNextAvailableId(kNextVerIdKey, &ver_id) || | 146 !ReadNextAvailableId(kNextVerIdKey, &ver_id) || |
| 89 !ReadNextAvailableId(kNextResIdKey, &res_id)) | 147 !ReadNextAvailableId(kNextResIdKey, &res_id)) |
| 90 return false; | 148 return false; |
| 91 | 149 |
| 92 *next_avail_registration_id = reg_id; | 150 *next_avail_registration_id = reg_id; |
| 93 *next_avail_version_id = ver_id; | 151 *next_avail_version_id = ver_id; |
| 94 *next_avail_resource_id = res_id; | 152 *next_avail_resource_id = res_id; |
| 95 return true; | 153 return true; |
| 96 } | 154 } |
| 97 | 155 |
| 156 bool ServiceWorkerDatabase::GetOriginsWithRegistrations( | |
| 157 std::set<GURL>* origins) { | |
| 158 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | |
| 159 DCHECK(origins); | |
| 160 | |
| 161 if (!LazyOpen(false) || is_disabled_) | |
| 162 return false; | |
| 163 | |
| 164 scoped_ptr<leveldb::Iterator> itr(db_->NewIterator(leveldb::ReadOptions())); | |
| 165 for (itr->Seek(kUniqueOriginKey); itr->Valid(); itr->Next()) { | |
| 166 if (!itr->status().ok()) { | |
| 167 HandleError(FROM_HERE, itr->status()); | |
| 168 origins->clear(); | |
| 169 return false; | |
| 170 } | |
| 171 | |
| 172 std::string key = itr->key().ToString(); | |
| 173 if (!StartsWithASCII(key, kUniqueOriginKey, true)) | |
| 174 break; | |
| 175 | |
| 176 std::string origin; | |
| 177 bool success = RemovePrefix(key, kUniqueOriginKey, &origin); | |
|
michaeln
2014/04/25 00:48:21
This function is funky? Either it doesn't need to
nhiroki
2014/04/25 11:01:05
Done.
| |
| 178 DCHECK(success); | |
| 179 origins->insert(GURL(origin)); | |
| 180 } | |
| 181 return true; | |
| 182 } | |
| 183 | |
| 184 bool ServiceWorkerDatabase::GetRegistrationsForOrigin( | |
| 185 const GURL& origin, | |
| 186 std::vector<ServiceWorkerRegistrationData>* registrations) { | |
| 187 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | |
| 188 DCHECK(registrations); | |
| 189 | |
| 190 if (!LazyOpen(false) || is_disabled_) | |
| 191 return false; | |
| 192 | |
| 193 // Create a key prefix for registrations. | |
| 194 std::string prefix = base::StringPrintf( | |
| 195 "%s%s%c", kRegKeyPrefix, origin.spec().c_str(), kKeySeparator); | |
| 196 | |
| 197 scoped_ptr<leveldb::Iterator> itr(db_->NewIterator(leveldb::ReadOptions())); | |
| 198 for (itr->Seek(prefix); itr->Valid(); itr->Next()) { | |
| 199 if (!itr->status().ok()) { | |
| 200 HandleError(FROM_HERE, itr->status()); | |
| 201 registrations->clear(); | |
| 202 return false; | |
| 203 } | |
| 204 | |
| 205 std::string key = itr->key().ToString(); | |
| 206 if (!StartsWithASCII(key, prefix, true)) | |
| 207 break; | |
| 208 | |
| 209 ServiceWorkerRegistrationData registration; | |
| 210 if (!ParseRegistrationData(itr->value().ToString(), ®istration)) { | |
| 211 HandleError(FROM_HERE, leveldb::Status::Corruption("failed to parse")); | |
| 212 registrations->clear(); | |
| 213 return false; | |
| 214 } | |
| 215 registrations->push_back(registration); | |
| 216 } | |
| 217 return true; | |
| 218 } | |
| 219 | |
| 220 bool ServiceWorkerDatabase::ReadRegistration( | |
| 221 int64 registration_id, | |
| 222 const GURL& origin, | |
| 223 ServiceWorkerRegistrationData* registration, | |
| 224 std::vector<ServiceWorkerResourceRecord>* resources) { | |
| 225 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | |
| 226 DCHECK(registration); | |
| 227 DCHECK(resources); | |
| 228 | |
| 229 if (!LazyOpen(false) || is_disabled_) | |
| 230 return false; | |
| 231 | |
| 232 ServiceWorkerRegistrationData value; | |
| 233 if (!ReadRegistrationData(registration_id, origin, &value)) | |
| 234 return false; | |
| 235 | |
| 236 // TODO(nhiroki): Read ResourceRecords tied with this registration. | |
| 237 | |
| 238 *registration = value; | |
| 239 return true; | |
| 240 } | |
| 241 | |
| 242 bool ServiceWorkerDatabase::WriteRegistration( | |
| 243 const ServiceWorkerRegistrationData& registration, | |
| 244 const std::vector<ServiceWorkerResourceRecord>& resources) { | |
| 245 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | |
| 246 if (!LazyOpen(true) || is_disabled_) | |
| 247 return false; | |
| 248 | |
| 249 leveldb::WriteBatch batch; | |
| 250 if (!BumpNextAvailableIdIfNeeded( | |
| 251 kNextRegIdKey, registration.registration_id(), &batch) || | |
| 252 !BumpNextAvailableIdIfNeeded( | |
| 253 kNextVerIdKey, registration.version_id(), &batch)) { | |
| 254 return false; | |
| 255 } | |
| 256 | |
| 257 PutUniqueOriginToBatch(GURL(registration.scope_url()).GetOrigin(), &batch); | |
|
michaeln
2014/04/25 00:48:21
is leveldb smart about noticing when a write doesn
nhiroki
2014/04/25 02:06:11
AFAICS, leveldb always writes log record even if t
nhiroki
2014/04/25 11:01:05
Let me address this in a following patch (added TO
| |
| 258 PutRegistrationDataToBatch(registration, &batch); | |
| 259 | |
| 260 // TODO(nhiroki): Write |resources| into the database. | |
| 261 return WriteBatch(&batch); | |
| 262 } | |
| 263 | |
| 264 bool ServiceWorkerDatabase::UpdateVersionToActive(int64 registration_id, | |
| 265 const GURL& origin) { | |
| 266 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | |
| 267 if (!LazyOpen(false) || is_disabled_) | |
| 268 return false; | |
| 269 | |
| 270 ServiceWorkerRegistrationData registration; | |
| 271 if (!ReadRegistrationData(registration_id, origin, ®istration)) | |
| 272 return false; | |
| 273 | |
| 274 registration.set_is_active(true); | |
| 275 | |
| 276 leveldb::WriteBatch batch; | |
| 277 PutRegistrationDataToBatch(registration, &batch); | |
| 278 return WriteBatch(&batch); | |
| 279 } | |
| 280 | |
| 281 bool ServiceWorkerDatabase::UpdateLastCheckTime(int64 registration_id, | |
| 282 const GURL& origin, | |
| 283 const base::Time& time) { | |
| 284 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | |
| 285 if (!LazyOpen(false) || is_disabled_) | |
| 286 return false; | |
| 287 | |
| 288 ServiceWorkerRegistrationData registration; | |
| 289 if (!ReadRegistrationData(registration_id, origin, ®istration)) | |
| 290 return false; | |
| 291 | |
| 292 DCHECK_LT(registration.last_update_check_time(), time.ToInternalValue()); | |
| 293 registration.set_last_update_check_time(time.ToInternalValue()); | |
| 294 | |
| 295 leveldb::WriteBatch batch; | |
| 296 PutRegistrationDataToBatch(registration, &batch); | |
| 297 return WriteBatch(&batch); | |
| 298 } | |
| 299 | |
| 300 bool ServiceWorkerDatabase::DeleteRegistration(int64 registration_id, | |
| 301 const GURL& origin) { | |
| 302 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | |
| 303 if (!LazyOpen(false) || is_disabled_) | |
| 304 return false; | |
| 305 | |
| 306 leveldb::WriteBatch batch; | |
| 307 | |
| 308 // Remove |origin| from unique origins if a registration specified by | |
| 309 // |registration_id| is the only one for |origin|. | |
| 310 // TODO(nhiroki): Check the uniqueness by more efficient way. | |
| 311 std::vector<ServiceWorkerRegistrationData> registrations; | |
| 312 if (!GetRegistrationsForOrigin(origin, ®istrations)) | |
| 313 return false; | |
| 314 if (registrations.size() == 1 && | |
| 315 registrations[0].registration_id() == registration_id) { | |
| 316 batch.Delete(CreateUniqueOriginKey(origin)); | |
| 317 } | |
| 318 | |
| 319 batch.Delete(CreateRegistrationKey(registration_id, origin)); | |
| 320 | |
| 321 // TODO(nhiroki): Delete ResourceRecords tied with this registration. | |
| 322 return WriteBatch(&batch); | |
| 323 } | |
| 324 | |
| 98 bool ServiceWorkerDatabase::LazyOpen(bool create_if_needed) { | 325 bool ServiceWorkerDatabase::LazyOpen(bool create_if_needed) { |
| 99 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); | 326 DCHECK(sequence_checker_.CalledOnValidSequencedThread()); |
| 100 if (IsOpen()) | 327 if (IsOpen()) |
| 101 return true; | 328 return true; |
| 102 | 329 |
| 103 // Do not try to open a database if we tried and failed once. | 330 // Do not try to open a database if we tried and failed once. |
| 104 if (is_disabled_) | 331 if (is_disabled_) |
| 105 return false; | 332 return false; |
| 106 | 333 |
| 107 // When |path_| is empty, open a database in-memory. | 334 // When |path_| is empty, open a database in-memory. |
| (...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 166 int64 parsed; | 393 int64 parsed; |
| 167 if (!base::StringToInt64(value, &parsed)) { | 394 if (!base::StringToInt64(value, &parsed)) { |
| 168 HandleError(FROM_HERE, leveldb::Status::Corruption("failed to parse")); | 395 HandleError(FROM_HERE, leveldb::Status::Corruption("failed to parse")); |
| 169 return false; | 396 return false; |
| 170 } | 397 } |
| 171 | 398 |
| 172 *next_avail_id = parsed; | 399 *next_avail_id = parsed; |
| 173 return true; | 400 return true; |
| 174 } | 401 } |
| 175 | 402 |
| 403 bool ServiceWorkerDatabase::ReadRegistrationData( | |
| 404 int64 registration_id, | |
| 405 const GURL& origin, | |
| 406 ServiceWorkerRegistrationData* registration) { | |
| 407 DCHECK(registration); | |
| 408 | |
| 409 std::string key = CreateRegistrationKey(registration_id, origin); | |
| 410 | |
| 411 std::string value; | |
| 412 leveldb::Status status = db_->Get(leveldb::ReadOptions(), key, &value); | |
| 413 if (!status.ok()) { | |
| 414 if (!status.IsNotFound()) | |
| 415 HandleError(FROM_HERE, status); | |
| 416 return false; | |
| 417 } | |
| 418 | |
| 419 ServiceWorkerRegistrationData parsed; | |
| 420 if (!ParseRegistrationData(value, &parsed)) { | |
| 421 HandleError(FROM_HERE, leveldb::Status::Corruption("failed to parse")); | |
| 422 return false; | |
| 423 } | |
| 424 | |
| 425 *registration = parsed; | |
| 426 return true; | |
| 427 } | |
| 428 | |
| 176 bool ServiceWorkerDatabase::WriteBatch(leveldb::WriteBatch* batch) { | 429 bool ServiceWorkerDatabase::WriteBatch(leveldb::WriteBatch* batch) { |
| 177 DCHECK(batch); | 430 DCHECK(batch); |
| 178 DCHECK(!is_disabled_); | 431 DCHECK(!is_disabled_); |
| 179 leveldb::Status status = db_->Write(leveldb::WriteOptions(), batch); | 432 leveldb::Status status = db_->Write(leveldb::WriteOptions(), batch); |
| 180 if (!status.ok()) { | 433 if (!status.ok()) { |
| 181 HandleError(FROM_HERE, status); | 434 HandleError(FROM_HERE, status); |
| 182 return false; | 435 return false; |
| 183 } | 436 } |
| 184 return true; | 437 return true; |
| 185 } | 438 } |
| 186 | 439 |
| 440 bool ServiceWorkerDatabase::BumpNextAvailableIdIfNeeded( | |
|
michaeln
2014/04/25 00:48:21
As coded this function will have problems if an id
nhiroki
2014/04/25 11:01:05
Good idea! Addressed it.
| |
| 441 const char* id_key, int64 used_id, leveldb::WriteBatch* batch) { | |
| 442 DCHECK(batch); | |
| 443 int64 next_available_id; | |
| 444 if (!ReadNextAvailableId(id_key, &next_available_id)) | |
| 445 return false; | |
| 446 if (next_available_id <= used_id) | |
| 447 batch->Put(id_key, base::Int64ToString(used_id + 1)); | |
| 448 return true; | |
| 449 } | |
| 450 | |
| 187 bool ServiceWorkerDatabase::IsOpen() { | 451 bool ServiceWorkerDatabase::IsOpen() { |
| 188 return db_.get() != NULL; | 452 return db_.get() != NULL; |
| 189 } | 453 } |
| 190 | 454 |
| 191 bool ServiceWorkerDatabase::IsEmpty() { | 455 bool ServiceWorkerDatabase::IsEmpty() { |
| 192 scoped_ptr<leveldb::Iterator> itr(db_->NewIterator(leveldb::ReadOptions())); | 456 scoped_ptr<leveldb::Iterator> itr(db_->NewIterator(leveldb::ReadOptions())); |
| 193 itr->SeekToFirst(); | 457 itr->SeekToFirst(); |
| 194 // TODO(nhiroki): Handle an error. | 458 // TODO(nhiroki): Handle an error. |
| 195 DCHECK(itr->status().ok()); | 459 DCHECK(itr->status().ok()); |
| 196 return !itr->Valid(); | 460 return !itr->Valid(); |
| 197 } | 461 } |
| 198 | 462 |
| 199 void ServiceWorkerDatabase::HandleError( | 463 void ServiceWorkerDatabase::HandleError( |
| 200 const tracked_objects::Location& from_here, | 464 const tracked_objects::Location& from_here, |
| 201 const leveldb::Status& status) { | 465 const leveldb::Status& status) { |
| 202 // TODO(nhiroki): Add an UMA histogram. | 466 // TODO(nhiroki): Add an UMA histogram. |
| 203 DLOG(ERROR) << "Failed at: " << from_here.ToString() | 467 DLOG(ERROR) << "Failed at: " << from_here.ToString() |
| 204 << " with error: " << status.ToString(); | 468 << " with error: " << status.ToString(); |
| 205 is_disabled_ = true; | 469 is_disabled_ = true; |
| 206 if (status.IsCorruption()) | 470 if (status.IsCorruption()) |
| 207 was_corruption_detected_ = true; | 471 was_corruption_detected_ = true; |
| 208 db_.reset(); | 472 db_.reset(); |
| 209 } | 473 } |
| 210 | 474 |
| 211 } // namespace content | 475 } // namespace content |
| OLD | NEW |