• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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