1// Protocol Buffers - Google's data interchange format 2// Copyright 2008 Google Inc. All rights reserved. 3// https://developers.google.com/protocol-buffers/ 4// 5// Redistribution and use in source and binary forms, with or without 6// modification, are permitted provided that the following conditions are 7// met: 8// 9// * Redistributions of source code must retain the above copyright 10// notice, this list of conditions and the following disclaimer. 11// * Redistributions in binary form must reproduce the above 12// copyright notice, this list of conditions and the following disclaimer 13// in the documentation and/or other materials provided with the 14// distribution. 15// * Neither the name of Google Inc. nor the names of its 16// contributors may be used to endorse or promote products derived from 17// this software without specific prior written permission. 18// 19// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 31goog.provide('jspb.Map'); 32 33goog.require('goog.asserts'); 34 35goog.forwardDeclare('jspb.BinaryReader'); 36goog.forwardDeclare('jspb.BinaryWriter'); 37 38 39 40/** 41 * Constructs a new Map. A Map is a container that is used to implement map 42 * fields on message objects. It closely follows the ES6 Map API; however, 43 * it is distinct because we do not want to depend on external polyfills or 44 * on ES6 itself. 45 * 46 * This constructor should only be called from generated message code. It is not 47 * intended for general use by library consumers. 48 * 49 * @template K, V 50 * 51 * @param {!Array<!Array<!Object>>} arr 52 * 53 * @param {?function(new:V)|function(new:V,?)=} opt_valueCtor 54 * The constructor for type V, if type V is a message type. 55 * 56 * @constructor 57 * @struct 58 */ 59jspb.Map = function(arr, opt_valueCtor) { 60 /** @const @private */ 61 this.arr_ = arr; 62 /** @const @private */ 63 this.valueCtor_ = opt_valueCtor; 64 65 /** @type {!Object<string, !jspb.Map.Entry_<K,V>>} @private */ 66 this.map_ = {}; 67 68 /** 69 * Is `this.arr_ updated with respect to `this.map_`? 70 * @type {boolean} 71 */ 72 this.arrClean = true; 73 74 if (this.arr_.length > 0) { 75 this.loadFromArray_(); 76 } 77}; 78 79 80/** 81 * Load initial content from underlying array. 82 * @private 83 */ 84jspb.Map.prototype.loadFromArray_ = function() { 85 for (var i = 0; i < this.arr_.length; i++) { 86 var record = this.arr_[i]; 87 var key = record[0]; 88 var value = record[1]; 89 this.map_[key.toString()] = new jspb.Map.Entry_(key, value); 90 } 91 this.arrClean = true; 92}; 93 94 95/** 96 * Synchronize content to underlying array, if needed, and return it. 97 * @return {!Array<!Array<!Object>>} 98 */ 99jspb.Map.prototype.toArray = function() { 100 if (this.arrClean) { 101 if (this.valueCtor_) { 102 // We need to recursively sync maps in submessages to their arrays. 103 var m = this.map_; 104 for (var p in m) { 105 if (Object.prototype.hasOwnProperty.call(m, p)) { 106 m[p].valueWrapper.toArray(); 107 } 108 } 109 } 110 } else { 111 // Delete all elements. 112 this.arr_.length = 0; 113 var strKeys = this.stringKeys_(); 114 // Output keys in deterministic (sorted) order. 115 strKeys.sort(); 116 for (var i = 0; i < strKeys.length; i++) { 117 var entry = this.map_[strKeys[i]]; 118 var valueWrapper = /** @type {!Object} */ (entry.valueWrapper); 119 if (valueWrapper) { 120 valueWrapper.toArray(); 121 } 122 this.arr_.push([entry.key, entry.value]); 123 } 124 this.arrClean = true; 125 } 126 return this.arr_; 127}; 128 129 130/** 131 * Helper: return an iterator over an array. 132 * @template T 133 * @param {!Array<T>} arr the array 134 * @return {!Iterator<T>} an iterator 135 * @private 136 */ 137jspb.Map.arrayIterator_ = function(arr) { 138 var idx = 0; 139 return /** @type {!Iterator} */ ({ 140 next: function() { 141 if (idx < arr.length) { 142 return { done: false, value: arr[idx++] }; 143 } else { 144 return { done: true }; 145 } 146 } 147 }); 148}; 149 150 151/** 152 * Returns the map's length (number of key/value pairs). 153 * @return {number} 154 */ 155jspb.Map.prototype.getLength = function() { 156 return this.stringKeys_().length; 157}; 158 159 160/** 161 * Clears the map. 162 */ 163jspb.Map.prototype.clear = function() { 164 this.map_ = {}; 165 this.arrClean = false; 166}; 167 168 169/** 170 * Deletes a particular key from the map. 171 * N.B.: differs in name from ES6 Map's `delete` because IE8 does not support 172 * reserved words as property names. 173 * @this {jspb.Map} 174 * @param {K} key 175 * @return {boolean} Whether any entry with this key was deleted. 176 */ 177jspb.Map.prototype.del = function(key) { 178 var keyValue = key.toString(); 179 var hadKey = this.map_.hasOwnProperty(keyValue); 180 delete this.map_[keyValue]; 181 this.arrClean = false; 182 return hadKey; 183}; 184 185 186/** 187 * Returns an array of [key, value] pairs in the map. 188 * 189 * This is redundant compared to the plain entries() method, but we provide this 190 * to help out Angular 1.x users. Still evaluating whether this is the best 191 * option. 192 * 193 * @return {!Array<K|V>} 194 */ 195jspb.Map.prototype.getEntryList = function() { 196 var entries = []; 197 var strKeys = this.stringKeys_(); 198 strKeys.sort(); 199 for (var i = 0; i < strKeys.length; i++) { 200 var entry = this.map_[strKeys[i]]; 201 entries.push([entry.key, entry.value]); 202 } 203 return entries; 204}; 205 206 207/** 208 * Returns an iterator over [key, value] pairs in the map. 209 * Closure compiler sadly doesn't support tuples, ie. Iterator<[K,V]>. 210 * @return {!Iterator<!Array<K|V>>} 211 * The iterator 212 */ 213jspb.Map.prototype.entries = function() { 214 var entries = []; 215 var strKeys = this.stringKeys_(); 216 strKeys.sort(); 217 for (var i = 0; i < strKeys.length; i++) { 218 var entry = this.map_[strKeys[i]]; 219 entries.push([entry.key, this.wrapEntry_(entry)]); 220 } 221 return jspb.Map.arrayIterator_(entries); 222}; 223 224 225/** 226 * Returns an iterator over keys in the map. 227 * @return {!Iterator<K>} The iterator 228 */ 229jspb.Map.prototype.keys = function() { 230 var keys = []; 231 var strKeys = this.stringKeys_(); 232 strKeys.sort(); 233 for (var i = 0; i < strKeys.length; i++) { 234 var entry = this.map_[strKeys[i]]; 235 keys.push(entry.key); 236 } 237 return jspb.Map.arrayIterator_(keys); 238}; 239 240 241/** 242 * Returns an iterator over values in the map. 243 * @return {!Iterator<V>} The iterator 244 */ 245jspb.Map.prototype.values = function() { 246 var values = []; 247 var strKeys = this.stringKeys_(); 248 strKeys.sort(); 249 for (var i = 0; i < strKeys.length; i++) { 250 var entry = this.map_[strKeys[i]]; 251 values.push(this.wrapEntry_(entry)); 252 } 253 return jspb.Map.arrayIterator_(values); 254}; 255 256 257/** 258 * Iterates over entries in the map, calling a function on each. 259 * @template T 260 * @param {function(this:T, V, K, ?jspb.Map<K, V>)} cb 261 * @param {T=} opt_thisArg 262 */ 263jspb.Map.prototype.forEach = function(cb, opt_thisArg) { 264 var strKeys = this.stringKeys_(); 265 strKeys.sort(); 266 for (var i = 0; i < strKeys.length; i++) { 267 var entry = this.map_[strKeys[i]]; 268 cb.call(opt_thisArg, this.wrapEntry_(entry), entry.key, this); 269 } 270}; 271 272 273/** 274 * Sets a key in the map to the given value. 275 * @param {K} key The key 276 * @param {V} value The value 277 * @return {!jspb.Map<K,V>} 278 */ 279jspb.Map.prototype.set = function(key, value) { 280 var entry = new jspb.Map.Entry_(key); 281 if (this.valueCtor_) { 282 entry.valueWrapper = value; 283 // .toArray() on a message returns a reference to the underlying array 284 // rather than a copy. 285 entry.value = value.toArray(); 286 } else { 287 entry.value = value; 288 } 289 this.map_[key.toString()] = entry; 290 this.arrClean = false; 291 return this; 292}; 293 294 295/** 296 * Helper: lazily construct a wrapper around an entry, if needed, and return the 297 * user-visible type. 298 * @param {!jspb.Map.Entry_<K,V>} entry 299 * @return {V} 300 * @private 301 */ 302jspb.Map.prototype.wrapEntry_ = function(entry) { 303 if (this.valueCtor_) { 304 if (!entry.valueWrapper) { 305 entry.valueWrapper = new this.valueCtor_(entry.value); 306 } 307 return /** @type {V} */ (entry.valueWrapper); 308 } else { 309 return entry.value; 310 } 311}; 312 313 314/** 315 * Gets the value corresponding to a key in the map. 316 * @param {K} key 317 * @return {V|undefined} The value, or `undefined` if key not present 318 */ 319jspb.Map.prototype.get = function(key) { 320 var keyValue = key.toString(); 321 var entry = this.map_[keyValue]; 322 if (entry) { 323 return this.wrapEntry_(entry); 324 } else { 325 return undefined; 326 } 327}; 328 329 330/** 331 * Determines whether the given key is present in the map. 332 * @param {K} key 333 * @return {boolean} `true` if the key is present 334 */ 335jspb.Map.prototype.has = function(key) { 336 var keyValue = key.toString(); 337 return (keyValue in this.map_); 338}; 339 340 341/** 342 * Write this Map field in wire format to a BinaryWriter, using the given field 343 * number. 344 * @param {number} fieldNumber 345 * @param {!jspb.BinaryWriter} writer 346 * @param {function(this:jspb.BinaryWriter,number,K)=} keyWriterFn 347 * The method on BinaryWriter that writes type K to the stream. 348 * @param {function(this:jspb.BinaryWriter,number,V)| 349 * function(this:jspb.BinaryReader,V,?)=} valueWriterFn 350 * The method on BinaryWriter that writes type V to the stream. May be 351 * writeMessage, in which case the second callback arg form is used. 352 * @param {?function(V,!jspb.BinaryWriter)=} opt_valueWriterCallback 353 * The BinaryWriter serialization callback for type V, if V is a message 354 * type. 355 */ 356jspb.Map.prototype.serializeBinary = function( 357 fieldNumber, writer, keyWriterFn, valueWriterFn, opt_valueWriterCallback) { 358 var strKeys = this.stringKeys_(); 359 strKeys.sort(); 360 for (var i = 0; i < strKeys.length; i++) { 361 var entry = this.map_[strKeys[i]]; 362 writer.beginSubMessage(fieldNumber); 363 keyWriterFn.call(writer, 1, entry.key); 364 if (this.valueCtor_) { 365 valueWriterFn.call(writer, 2, this.wrapEntry_(entry), 366 opt_valueWriterCallback); 367 } else { 368 valueWriterFn_.call(writer, 2, entry.value); 369 } 370 writer.endSubMessage(); 371 } 372}; 373 374 375/** 376 * Read one key/value message from the given BinaryReader. Compatible as the 377 * `reader` callback parameter to jspb.BinaryReader.readMessage, to be called 378 * when a key/value pair submessage is encountered. 379 * @param {!jspb.Map} map 380 * @param {!jspb.BinaryReader} reader 381 * @param {function(this:jspb.BinaryReader):K=} keyReaderFn 382 * The method on BinaryReader that reads type K from the stream. 383 * 384 * @param {function(this:jspb.BinaryReader):V| 385 * function(this:jspb.BinaryReader,V, 386 * function(V,!jspb.BinaryReader))=} valueReaderFn 387 * The method on BinaryReader that reads type V from the stream. May be 388 * readMessage, in which case the second callback arg form is used. 389 * 390 * @param {?function(V,!jspb.BinaryReader)=} opt_valueReaderCallback 391 * The BinaryReader parsing callback for type V, if V is a message type. 392 * 393 */ 394jspb.Map.deserializeBinary = function(map, reader, keyReaderFn, valueReaderFn, 395 opt_valueReaderCallback) { 396 var key = undefined; 397 var value = undefined; 398 399 while (reader.nextField()) { 400 if (reader.isEndGroup()) { 401 break; 402 } 403 var field = reader.getFieldNumber(); 404 if (field == 1) { 405 // Key. 406 key = keyReaderFn.call(reader); 407 } else if (field == 2) { 408 // Value. 409 if (map.valueCtor_) { 410 value = new map.valueCtor_(); 411 valueReaderFn.call(reader, value, opt_valueReaderCallback); 412 } else { 413 value = valueReaderFn.call(reader); 414 } 415 } 416 } 417 418 goog.asserts.assert(key != undefined); 419 goog.asserts.assert(value != undefined); 420 map.set(key, value); 421}; 422 423 424/** 425 * Helper: compute the list of all stringified keys in the underlying Object 426 * map. 427 * @return {!Array<string>} 428 * @private 429 */ 430jspb.Map.prototype.stringKeys_ = function() { 431 var m = this.map_; 432 var ret = []; 433 for (var p in m) { 434 if (Object.prototype.hasOwnProperty.call(m, p)) { 435 ret.push(p); 436 } 437 } 438 return ret; 439}; 440 441 442 443/** 444 * @param {!K} key The entry's key. 445 * @param {V=} opt_value The entry's value wrapper. 446 * @constructor 447 * @struct 448 * @template K, V 449 * @private 450 */ 451jspb.Map.Entry_ = function(key, opt_value) { 452 /** @const {K} */ 453 this.key = key; 454 455 // The JSPB-serializable value. For primitive types this will be of type V. 456 // For message types it will be an array. 457 /** @type {V} */ 458 this.value = opt_value; 459 460 // Only used for submessage values. 461 /** @type {V} */ 462 this.valueWrapper = undefined; 463}; 464