• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Protocol Buffers - Google's data interchange format
3  * Copyright 2014 Google Inc.  All rights reserved.
4  * https://developers.google.com/protocol-buffers/
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are
8  * met:
9  *
10  *     * Redistributions of source code must retain the above copyright
11  * notice, this list of conditions and the following disclaimer.
12  *     * Redistributions in binary form must reproduce the above
13  * copyright notice, this list of conditions and the following disclaimer
14  * in the documentation and/or other materials provided with the
15  * distribution.
16  *     * Neither the name of Google Inc. nor the names of its
17  * contributors may be used to endorse or promote products derived from
18  * this software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 package com.google.protobuf.jruby;
34 
35 import com.google.protobuf.Descriptors.FieldDescriptor;
36 import com.google.protobuf.DynamicMessage;
37 import java.nio.ByteBuffer;
38 import java.security.MessageDigest;
39 import java.security.NoSuchAlgorithmException;
40 import java.util.ArrayList;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import org.jruby.*;
45 import org.jruby.anno.JRubyClass;
46 import org.jruby.anno.JRubyMethod;
47 import org.jruby.runtime.Block;
48 import org.jruby.runtime.Helpers;
49 import org.jruby.runtime.ObjectAllocator;
50 import org.jruby.runtime.ThreadContext;
51 import org.jruby.runtime.builtin.IRubyObject;
52 
53 @JRubyClass(name = "Map", include = "Enumerable")
54 public class RubyMap extends RubyObject {
createRubyMap(Ruby runtime)55   public static void createRubyMap(Ruby runtime) {
56     RubyModule protobuf = runtime.getClassFromPath("Google::Protobuf");
57     RubyClass cMap =
58         protobuf.defineClassUnder(
59             "Map",
60             runtime.getObject(),
61             new ObjectAllocator() {
62               @Override
63               public IRubyObject allocate(Ruby ruby, RubyClass rubyClass) {
64                 return new RubyMap(ruby, rubyClass);
65               }
66             });
67     cMap.includeModule(runtime.getEnumerable());
68     cMap.defineAnnotatedMethods(RubyMap.class);
69   }
70 
RubyMap(Ruby ruby, RubyClass rubyClass)71   public RubyMap(Ruby ruby, RubyClass rubyClass) {
72     super(ruby, rubyClass);
73   }
74 
75   /*
76    * call-seq:
77    *     Map.new(key_type, value_type, value_typeclass = nil, init_hashmap = {})
78    *     => new map
79    *
80    * Allocates a new Map container. This constructor may be called with 2, 3, or 4
81    * arguments. The first two arguments are always present and are symbols (taking
82    * on the same values as field-type symbols in message descriptors) that
83    * indicate the type of the map key and value fields.
84    *
85    * The supported key types are: :int32, :int64, :uint32, :uint64, :fixed32,
86    * :fixed64, :sfixed32, :sfixed64, :sint32, :sint64, :bool, :string, :bytes.
87    *
88    * The supported value types are: :int32, :int64, :uint32, :uint64, :fixed32,
89    * :fixed64, :sfixed32, :sfixed64, :sint32, :sint64, :bool, :string, :bytes,
90    * :enum, :message.
91    *
92    * The third argument, value_typeclass, must be present if value_type is :enum
93    * or :message. As in RepeatedField#new, this argument must be a message class
94    * (for :message) or enum module (for :enum).
95    *
96    * The last argument, if present, provides initial content for map. Note that
97    * this may be an ordinary Ruby hashmap or another Map instance with identical
98    * key and value types. Also note that this argument may be present whether or
99    * not value_typeclass is present (and it is unambiguously separate from
100    * value_typeclass because value_typeclass's presence is strictly determined by
101    * value_type). The contents of this initial hashmap or Map instance are
102    * shallow-copied into the new Map: the original map is unmodified, but
103    * references to underlying objects will be shared if the value type is a
104    * message type.
105    */
106   @JRubyMethod(required = 2, optional = 2)
initialize(ThreadContext context, IRubyObject[] args)107   public IRubyObject initialize(ThreadContext context, IRubyObject[] args) {
108     this.table = new HashMap<IRubyObject, IRubyObject>();
109     this.keyType = Utils.rubyToFieldType(args[0]);
110     this.valueType = Utils.rubyToFieldType(args[1]);
111 
112     switch (keyType) {
113       case STRING:
114       case BYTES:
115         this.keyTypeIsString = true;
116         break;
117       case INT32:
118       case INT64:
119       case SINT32:
120       case SINT64:
121       case UINT32:
122       case UINT64:
123       case FIXED32:
124       case FIXED64:
125       case SFIXED32:
126       case SFIXED64:
127       case BOOL:
128         // These are OK.
129         break;
130       default:
131         throw context.runtime.newArgumentError("Invalid key type for map.");
132     }
133 
134     int initValueArg = 2;
135     if (needTypeclass(this.valueType) && args.length > 2) {
136       this.valueTypeClass = args[2];
137       Utils.validateTypeClass(context, this.valueType, this.valueTypeClass);
138       initValueArg = 3;
139     } else {
140       this.valueTypeClass = context.runtime.getNilClass();
141     }
142 
143     if (args.length > initValueArg) {
144       mergeIntoSelf(context, args[initValueArg]);
145     }
146     return this;
147   }
148 
149   /*
150    * call-seq:
151    *     Map.[]=(key, value) => value
152    *
153    * Inserts or overwrites the value at the given key with the given new value.
154    * Throws an exception if the key type is incorrect. Returns the new value that
155    * was just inserted.
156    */
157   @JRubyMethod(name = "[]=")
indexSet(ThreadContext context, IRubyObject key, IRubyObject value)158   public IRubyObject indexSet(ThreadContext context, IRubyObject key, IRubyObject value) {
159     checkFrozen();
160 
161     /*
162      * String types for keys return a different error than
163      * other types for keys, so deal with them specifically first
164      */
165     if (keyTypeIsString && !(key instanceof RubySymbol || key instanceof RubyString)) {
166       throw Utils.createTypeError(context, "Expected string for map key");
167     }
168     key = Utils.checkType(context, keyType, "key", key, (RubyModule) valueTypeClass);
169     value = Utils.checkType(context, valueType, "value", value, (RubyModule) valueTypeClass);
170     IRubyObject symbol;
171     if (valueType == FieldDescriptor.Type.ENUM
172         && Utils.isRubyNum(value)
173         && !(symbol = RubyEnum.lookup(context, valueTypeClass, value)).isNil()) {
174       value = symbol;
175     }
176     this.table.put(key, value);
177     return value;
178   }
179 
180   /*
181    * call-seq:
182    *     Map.[](key) => value
183    *
184    * Accesses the element at the given key. Throws an exception if the key type is
185    * incorrect. Returns nil when the key is not present in the map.
186    */
187   @JRubyMethod(name = "[]")
index(ThreadContext context, IRubyObject key)188   public IRubyObject index(ThreadContext context, IRubyObject key) {
189     key = Utils.symToString(key);
190     return Helpers.nullToNil(table.get(key), context.nil);
191   }
192 
193   /*
194    * call-seq:
195    *     Map.==(other) => boolean
196    *
197    * Compares this map to another. Maps are equal if they have identical key sets,
198    * and for each key, the values in both maps compare equal. Elements are
199    * compared as per normal Ruby semantics, by calling their :== methods (or
200    * performing a more efficient comparison for primitive types).
201    *
202    * Maps with dissimilar key types or value types/typeclasses are never equal,
203    * even if value comparison (for example, between integers and floats) would
204    * have otherwise indicated that every element has equal value.
205    */
206   @JRubyMethod(name = "==")
eq(ThreadContext context, IRubyObject _other)207   public IRubyObject eq(ThreadContext context, IRubyObject _other) {
208     if (_other instanceof RubyHash) return singleLevelHash(context).op_equal(context, _other);
209     RubyMap other = (RubyMap) _other;
210     if (this == other) return context.runtime.getTrue();
211     if (!typeCompatible(other) || this.table.size() != other.table.size())
212       return context.runtime.getFalse();
213     for (IRubyObject key : table.keySet()) {
214       if (!other.table.containsKey(key)) return context.runtime.getFalse();
215       if (!other.table.get(key).equals(table.get(key))) return context.runtime.getFalse();
216     }
217     return context.runtime.getTrue();
218   }
219 
220   /*
221    * call-seq:
222    *     Map.inspect => string
223    *
224    * Returns a string representing this map's elements. It will be formatted as
225    * "{key => value, key => value, ...}", with each key and value string
226    * representation computed by its own #inspect method.
227    */
228   @JRubyMethod
inspect()229   public IRubyObject inspect() {
230     return singleLevelHash(getRuntime().getCurrentContext()).inspect();
231   }
232 
233   /*
234    * call-seq:
235    *     Map.hash => hash_value
236    *
237    * Returns a hash value based on this map's contents.
238    */
239   @JRubyMethod
hash(ThreadContext context)240   public IRubyObject hash(ThreadContext context) {
241     try {
242       MessageDigest digest = MessageDigest.getInstance("SHA-256");
243       for (IRubyObject key : table.keySet()) {
244         digest.update((byte) key.hashCode());
245         digest.update((byte) table.get(key).hashCode());
246       }
247       return context.runtime.newFixnum(ByteBuffer.wrap(digest.digest()).getLong());
248     } catch (NoSuchAlgorithmException ignore) {
249       return context.runtime.newFixnum(System.identityHashCode(table));
250     }
251   }
252 
253   /*
254    * call-seq:
255    *     Map.keys => [list_of_keys]
256    *
257    * Returns the list of keys contained in the map, in unspecified order.
258    */
259   @JRubyMethod
keys(ThreadContext context)260   public IRubyObject keys(ThreadContext context) {
261     return RubyArray.newArray(context.runtime, table.keySet());
262   }
263 
264   /*
265    * call-seq:
266    *     Map.values => [list_of_values]
267    *
268    * Returns the list of values contained in the map, in unspecified order.
269    */
270   @JRubyMethod
values(ThreadContext context)271   public IRubyObject values(ThreadContext context) {
272     return RubyArray.newArray(context.runtime, table.values());
273   }
274 
275   /*
276    * call-seq:
277    *     Map.clear
278    *
279    * Removes all entries from the map.
280    */
281   @JRubyMethod
clear(ThreadContext context)282   public IRubyObject clear(ThreadContext context) {
283     checkFrozen();
284     table.clear();
285     return context.nil;
286   }
287 
288   /*
289    * call-seq:
290    *     Map.each(&block)
291    *
292    * Invokes &block on each |key, value| pair in the map, in unspecified order.
293    * Note that Map also includes Enumerable; map thus acts like a normal Ruby
294    * sequence.
295    */
296   @JRubyMethod
each(ThreadContext context, Block block)297   public IRubyObject each(ThreadContext context, Block block) {
298     for (IRubyObject key : table.keySet()) {
299       block.yieldSpecific(context, key, table.get(key));
300     }
301     return context.nil;
302   }
303 
304   /*
305    * call-seq:
306    *     Map.delete(key) => old_value
307    *
308    * Deletes the value at the given key, if any, returning either the old value or
309    * nil if none was present. Throws an exception if the key is of the wrong type.
310    */
311   @JRubyMethod
delete(ThreadContext context, IRubyObject key)312   public IRubyObject delete(ThreadContext context, IRubyObject key) {
313     checkFrozen();
314     return table.remove(key);
315   }
316 
317   /*
318    * call-seq:
319    *     Map.has_key?(key) => bool
320    *
321    * Returns true if the given key is present in the map. Throws an exception if
322    * the key has the wrong type.
323    */
324   @JRubyMethod(name = "has_key?")
hasKey(ThreadContext context, IRubyObject key)325   public IRubyObject hasKey(ThreadContext context, IRubyObject key) {
326     return this.table.containsKey(key) ? context.runtime.getTrue() : context.runtime.getFalse();
327   }
328 
329   /*
330    * call-seq:
331    *     Map.length
332    *
333    * Returns the number of entries (key-value pairs) in the map.
334    */
335   @JRubyMethod(name = {"length", "size"})
length(ThreadContext context)336   public IRubyObject length(ThreadContext context) {
337     return context.runtime.newFixnum(this.table.size());
338   }
339 
340   /*
341    * call-seq:
342    *     Map.dup => new_map
343    *
344    * Duplicates this map with a shallow copy. References to all non-primitive
345    * element objects (e.g., submessages) are shared.
346    */
347   @JRubyMethod
dup(ThreadContext context)348   public IRubyObject dup(ThreadContext context) {
349     RubyMap newMap = newThisType(context);
350     for (Map.Entry<IRubyObject, IRubyObject> entry : table.entrySet()) {
351       newMap.table.put(entry.getKey(), entry.getValue());
352     }
353     return newMap;
354   }
355 
356   @JRubyMethod(name = "to_h")
toHash(ThreadContext context)357   public RubyHash toHash(ThreadContext context) {
358     Map<IRubyObject, IRubyObject> mapForHash = new HashMap();
359 
360     table.forEach(
361         (key, value) -> {
362           if (!value.isNil()) {
363             if (value.respondsTo("to_h")) {
364               value = Helpers.invoke(context, value, "to_h");
365             } else if (value.respondsTo("to_a")) {
366               value = Helpers.invoke(context, value, "to_a");
367             }
368             mapForHash.put(key, value);
369           }
370         });
371 
372     return RubyHash.newHash(context.runtime, mapForHash, context.nil);
373   }
374 
375   @JRubyMethod
freeze(ThreadContext context)376   public IRubyObject freeze(ThreadContext context) {
377     if (isFrozen()) {
378       return this;
379     }
380     setFrozen(true);
381     if (valueType == FieldDescriptor.Type.MESSAGE) {
382       for (IRubyObject key : table.keySet()) {
383         ((RubyMessage) table.get(key)).freeze(context);
384       }
385     }
386     return this;
387   }
388 
389   // Used by Google::Protobuf.deep_copy but not exposed directly.
deepCopy(ThreadContext context)390   protected IRubyObject deepCopy(ThreadContext context) {
391     RubyMap newMap = newThisType(context);
392     switch (valueType) {
393       case MESSAGE:
394         for (IRubyObject key : table.keySet()) {
395           RubyMessage message = (RubyMessage) table.get(key);
396           newMap.table.put(key.dup(), message.deepCopy(context));
397         }
398         break;
399       default:
400         for (IRubyObject key : table.keySet()) {
401           newMap.table.put(key.dup(), table.get(key).dup());
402         }
403     }
404     return newMap;
405   }
406 
build( ThreadContext context, RubyDescriptor descriptor, int depth, int recursionLimit)407   protected List<DynamicMessage> build(
408       ThreadContext context, RubyDescriptor descriptor, int depth, int recursionLimit) {
409     List<DynamicMessage> list = new ArrayList<DynamicMessage>();
410     RubyClass rubyClass = (RubyClass) descriptor.msgclass(context);
411     FieldDescriptor keyField = descriptor.getField("key");
412     FieldDescriptor valueField = descriptor.getField("value");
413     for (IRubyObject key : table.keySet()) {
414       RubyMessage mapMessage = (RubyMessage) rubyClass.newInstance(context, Block.NULL_BLOCK);
415       mapMessage.setField(context, keyField, key);
416       mapMessage.setField(context, valueField, table.get(key));
417       list.add(mapMessage.build(context, depth + 1, recursionLimit));
418     }
419     return list;
420   }
421 
mergeIntoSelf(final ThreadContext context, IRubyObject hashmap)422   protected RubyMap mergeIntoSelf(final ThreadContext context, IRubyObject hashmap) {
423     if (hashmap instanceof RubyHash) {
424       ((RubyHash) hashmap)
425           .visitAll(
426               context,
427               new RubyHash.Visitor() {
428                 @Override
429                 public void visit(IRubyObject key, IRubyObject val) {
430                   if (val instanceof RubyHash && !valueTypeClass.isNil()) {
431                     val = ((RubyClass) valueTypeClass).newInstance(context, val, Block.NULL_BLOCK);
432                   }
433                   indexSet(context, key, val);
434                 }
435               },
436               null);
437     } else if (hashmap instanceof RubyMap) {
438       RubyMap other = (RubyMap) hashmap;
439       if (!typeCompatible(other)) {
440         throw Utils.createTypeError(context, "Attempt to merge Map with mismatching types");
441       }
442     } else {
443       throw Utils.createTypeError(context, "Unknown type merging into Map");
444     }
445     return this;
446   }
447 
typeCompatible(RubyMap other)448   protected boolean typeCompatible(RubyMap other) {
449     return this.keyType == other.keyType
450         && this.valueType == other.valueType
451         && this.valueTypeClass == other.valueTypeClass;
452   }
453 
newThisType(ThreadContext context)454   private RubyMap newThisType(ThreadContext context) {
455     RubyMap newMap;
456     if (needTypeclass(valueType)) {
457       newMap =
458           (RubyMap)
459               metaClass.newInstance(
460                   context,
461                   Utils.fieldTypeToRuby(context, keyType),
462                   Utils.fieldTypeToRuby(context, valueType),
463                   valueTypeClass,
464                   Block.NULL_BLOCK);
465     } else {
466       newMap =
467           (RubyMap)
468               metaClass.newInstance(
469                   context,
470                   Utils.fieldTypeToRuby(context, keyType),
471                   Utils.fieldTypeToRuby(context, valueType),
472                   Block.NULL_BLOCK);
473     }
474     newMap.table = new HashMap<IRubyObject, IRubyObject>();
475     return newMap;
476   }
477 
478   /*
479    * toHash calls toHash on values, for some camparisons we only need
480    * a hash with the original objects still as values
481    */
singleLevelHash(ThreadContext context)482   private RubyHash singleLevelHash(ThreadContext context) {
483     return RubyHash.newHash(context.runtime, table, context.nil);
484   }
485 
needTypeclass(FieldDescriptor.Type type)486   private boolean needTypeclass(FieldDescriptor.Type type) {
487     switch (type) {
488       case MESSAGE:
489       case ENUM:
490         return true;
491       default:
492         return false;
493     }
494   }
495 
496   private FieldDescriptor.Type keyType;
497   private FieldDescriptor.Type valueType;
498   private IRubyObject valueTypeClass;
499   private Map<IRubyObject, IRubyObject> table;
500   private boolean keyTypeIsString = false;
501 }
502