• 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;
36 import com.google.protobuf.DynamicMessage;
37 import com.google.protobuf.MapEntry;
38 import org.jruby.*;
39 import org.jruby.anno.JRubyClass;
40 import org.jruby.anno.JRubyMethod;
41 import org.jruby.internal.runtime.methods.DynamicMethod;
42 import org.jruby.runtime.Block;
43 import org.jruby.runtime.ObjectAllocator;
44 import org.jruby.runtime.ThreadContext;
45 import org.jruby.runtime.builtin.IRubyObject;
46 import org.jruby.util.ByteList;
47 
48 import java.security.MessageDigest;
49 import java.security.NoSuchAlgorithmException;
50 import java.util.ArrayList;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Map;
54 
55 @JRubyClass(name = "Map", include = "Enumerable")
56 public class RubyMap extends RubyObject {
createRubyMap(Ruby runtime)57     public static void createRubyMap(Ruby runtime) {
58         RubyModule protobuf = runtime.getClassFromPath("Google::Protobuf");
59         RubyClass cMap = protobuf.defineClassUnder("Map", runtime.getObject(), new ObjectAllocator() {
60             @Override
61             public IRubyObject allocate(Ruby ruby, RubyClass rubyClass) {
62                 return new RubyMap(ruby, rubyClass);
63             }
64         });
65         cMap.includeModule(runtime.getEnumerable());
66         cMap.defineAnnotatedMethods(RubyMap.class);
67     }
68 
RubyMap(Ruby ruby, RubyClass rubyClass)69     public RubyMap(Ruby ruby, RubyClass rubyClass) {
70         super(ruby, rubyClass);
71     }
72 
73     /*
74      * call-seq:
75      *     Map.new(key_type, value_type, value_typeclass = nil, init_hashmap = {})
76      *     => new map
77      *
78      * Allocates a new Map container. This constructor may be called with 2, 3, or 4
79      * arguments. The first two arguments are always present and are symbols (taking
80      * on the same values as field-type symbols in message descriptors) that
81      * indicate the type of the map key and value fields.
82      *
83      * The supported key types are: :int32, :int64, :uint32, :uint64, :bool,
84      * :string, :bytes.
85      *
86      * The supported value types are: :int32, :int64, :uint32, :uint64, :bool,
87      * :string, :bytes, :enum, :message.
88      *
89      * The third argument, value_typeclass, must be present if value_type is :enum
90      * or :message. As in RepeatedField#new, this argument must be a message class
91      * (for :message) or enum module (for :enum).
92      *
93      * The last argument, if present, provides initial content for map. Note that
94      * this may be an ordinary Ruby hashmap or another Map instance with identical
95      * key and value types. Also note that this argument may be present whether or
96      * not value_typeclass is present (and it is unambiguously separate from
97      * value_typeclass because value_typeclass's presence is strictly determined by
98      * value_type). The contents of this initial hashmap or Map instance are
99      * shallow-copied into the new Map: the original map is unmodified, but
100      * references to underlying objects will be shared if the value type is a
101      * message type.
102      */
103 
104     @JRubyMethod(required = 2, optional = 2)
initialize(ThreadContext context, IRubyObject[] args)105     public IRubyObject initialize(ThreadContext context, IRubyObject[] args) {
106         this.table = new HashMap<IRubyObject, IRubyObject>();
107         this.keyType = Utils.rubyToFieldType(args[0]);
108         this.valueType = Utils.rubyToFieldType(args[1]);
109 
110         switch(keyType) {
111             case INT32:
112             case INT64:
113             case UINT32:
114             case UINT64:
115             case BOOL:
116             case STRING:
117             case BYTES:
118                 // These are OK.
119                 break;
120             default:
121                 throw context.runtime.newArgumentError("Invalid key type for map.");
122         }
123 
124         int initValueArg = 2;
125         if (needTypeclass(this.valueType) && args.length > 2) {
126             this.valueTypeClass = args[2];
127             Utils.validateTypeClass(context, this.valueType, this.valueTypeClass);
128             initValueArg = 3;
129         } else {
130             this.valueTypeClass = context.runtime.getNilClass();
131         }
132 
133         // Table value type is always UINT64: this ensures enough space to store the
134         // native_slot value.
135         if (args.length > initValueArg) {
136             mergeIntoSelf(context, args[initValueArg]);
137         }
138         return this;
139     }
140 
141     /*
142      * call-seq:
143      *     Map.[]=(key, value) => value
144      *
145      * Inserts or overwrites the value at the given key with the given new value.
146      * Throws an exception if the key type is incorrect. Returns the new value that
147      * was just inserted.
148      */
149     @JRubyMethod(name = "[]=")
indexSet(ThreadContext context, IRubyObject key, IRubyObject value)150     public IRubyObject indexSet(ThreadContext context, IRubyObject key, IRubyObject value) {
151         key = Utils.checkType(context, keyType, key, (RubyModule) valueTypeClass);
152         value = Utils.checkType(context, valueType, value, (RubyModule) valueTypeClass);
153         IRubyObject symbol;
154         if (valueType == Descriptors.FieldDescriptor.Type.ENUM &&
155                 Utils.isRubyNum(value) &&
156                 ! (symbol = RubyEnum.lookup(context, valueTypeClass, value)).isNil()) {
157             value = symbol;
158         }
159         this.table.put(key, value);
160         return value;
161     }
162 
163     /*
164      * call-seq:
165      *     Map.[](key) => value
166      *
167      * Accesses the element at the given key. Throws an exception if the key type is
168      * incorrect. Returns nil when the key is not present in the map.
169      */
170     @JRubyMethod(name = "[]")
index(ThreadContext context, IRubyObject key)171     public IRubyObject index(ThreadContext context, IRubyObject key) {
172         if (table.containsKey(key))
173             return this.table.get(key);
174         return context.runtime.getNil();
175     }
176 
177     /*
178      * call-seq:
179      *     Map.==(other) => boolean
180      *
181      * Compares this map to another. Maps are equal if they have identical key sets,
182      * and for each key, the values in both maps compare equal. Elements are
183      * compared as per normal Ruby semantics, by calling their :== methods (or
184      * performing a more efficient comparison for primitive types).
185      *
186      * Maps with dissimilar key types or value types/typeclasses are never equal,
187      * even if value comparison (for example, between integers and floats) would
188      * have otherwise indicated that every element has equal value.
189      */
190     @JRubyMethod(name = "==")
eq(ThreadContext context, IRubyObject _other)191     public IRubyObject eq(ThreadContext context, IRubyObject _other) {
192         if (_other instanceof RubyHash)
193             return toHash(context).op_equal(context, _other);
194         RubyMap other = (RubyMap) _other;
195         if (this == other) return context.runtime.getTrue();
196         if (!typeCompatible(other) || this.table.size() != other.table.size())
197             return context.runtime.getFalse();
198         for (IRubyObject key : table.keySet()) {
199             if (! other.table.containsKey(key))
200                 return context.runtime.getFalse();
201             if (! other.table.get(key).equals(table.get(key)))
202                 return context.runtime.getFalse();
203         }
204         return context.runtime.getTrue();
205     }
206 
207     /*
208      * call-seq:
209      *     Map.inspect => string
210      *
211      * Returns a string representing this map's elements. It will be formatted as
212      * "{key => value, key => value, ...}", with each key and value string
213      * representation computed by its own #inspect method.
214      */
215     @JRubyMethod
inspect()216     public IRubyObject inspect() {
217         return toHash(getRuntime().getCurrentContext()).inspect();
218     }
219 
220     /*
221      * call-seq:
222      *     Map.hash => hash_value
223      *
224      * Returns a hash value based on this map's contents.
225      */
226     @JRubyMethod
hash(ThreadContext context)227     public IRubyObject hash(ThreadContext context) {
228         try {
229             MessageDigest digest = MessageDigest.getInstance("SHA-256");
230             for (IRubyObject key : table.keySet()) {
231                 digest.update((byte) key.hashCode());
232                 digest.update((byte) table.get(key).hashCode());
233             }
234             return context.runtime.newString(new ByteList(digest.digest()));
235         } catch (NoSuchAlgorithmException ignore) {
236             return context.runtime.newFixnum(System.identityHashCode(table));
237         }
238     }
239 
240     /*
241      * call-seq:
242      *     Map.keys => [list_of_keys]
243      *
244      * Returns the list of keys contained in the map, in unspecified order.
245      */
246     @JRubyMethod
keys(ThreadContext context)247     public IRubyObject keys(ThreadContext context) {
248         return RubyArray.newArray(context.runtime, table.keySet());
249     }
250 
251     /*
252      * call-seq:
253      *     Map.values => [list_of_values]
254      *
255      * Returns the list of values contained in the map, in unspecified order.
256      */
257     @JRubyMethod
values(ThreadContext context)258     public IRubyObject values(ThreadContext context) {
259         return RubyArray.newArray(context.runtime, table.values());
260     }
261 
262     /*
263      * call-seq:
264      *     Map.clear
265      *
266      * Removes all entries from the map.
267      */
268     @JRubyMethod
clear(ThreadContext context)269     public IRubyObject clear(ThreadContext context) {
270         table.clear();
271         return context.runtime.getNil();
272     }
273 
274     /*
275      * call-seq:
276      *     Map.each(&block)
277      *
278      * Invokes &block on each |key, value| pair in the map, in unspecified order.
279      * Note that Map also includes Enumerable; map thus acts like a normal Ruby
280      * sequence.
281      */
282     @JRubyMethod
each(ThreadContext context, Block block)283     public IRubyObject each(ThreadContext context, Block block) {
284         for (IRubyObject key : table.keySet()) {
285             block.yieldSpecific(context, key, table.get(key));
286         }
287         return context.runtime.getNil();
288     }
289 
290     /*
291      * call-seq:
292      *     Map.delete(key) => old_value
293      *
294      * Deletes the value at the given key, if any, returning either the old value or
295      * nil if none was present. Throws an exception if the key is of the wrong type.
296      */
297     @JRubyMethod
delete(ThreadContext context, IRubyObject key)298     public IRubyObject delete(ThreadContext context, IRubyObject key) {
299         return table.remove(key);
300     }
301 
302     /*
303      * call-seq:
304      *     Map.has_key?(key) => bool
305      *
306      * Returns true if the given key is present in the map. Throws an exception if
307      * the key has the wrong type.
308      */
309     @JRubyMethod(name = "has_key?")
hasKey(ThreadContext context, IRubyObject key)310     public IRubyObject hasKey(ThreadContext context, IRubyObject key) {
311         return this.table.containsKey(key) ? context.runtime.getTrue() : context.runtime.getFalse();
312     }
313 
314     /*
315      * call-seq:
316      *     Map.length
317      *
318      * Returns the number of entries (key-value pairs) in the map.
319      */
320     @JRubyMethod
length(ThreadContext context)321     public IRubyObject length(ThreadContext context) {
322         return context.runtime.newFixnum(this.table.size());
323     }
324 
325     /*
326      * call-seq:
327      *     Map.dup => new_map
328      *
329      * Duplicates this map with a shallow copy. References to all non-primitive
330      * element objects (e.g., submessages) are shared.
331      */
332     @JRubyMethod
dup(ThreadContext context)333     public IRubyObject dup(ThreadContext context) {
334         RubyMap newMap = newThisType(context);
335         for (Map.Entry<IRubyObject, IRubyObject> entry : table.entrySet()) {
336             newMap.table.put(entry.getKey(), entry.getValue());
337         }
338         return newMap;
339     }
340 
341     @JRubyMethod(name = "to_h")
toHash(ThreadContext context)342     public RubyHash toHash(ThreadContext context) {
343         return RubyHash.newHash(context.runtime, table, context.runtime.getNil());
344     }
345 
346     // Used by Google::Protobuf.deep_copy but not exposed directly.
deepCopy(ThreadContext context)347     protected IRubyObject deepCopy(ThreadContext context) {
348         RubyMap newMap = newThisType(context);
349         switch (valueType) {
350             case MESSAGE:
351                 for (IRubyObject key : table.keySet()) {
352                     RubyMessage message = (RubyMessage) table.get(key);
353                     newMap.table.put(key.dup(), message.deepCopy(context));
354                 }
355                 break;
356             default:
357                 for (IRubyObject key : table.keySet()) {
358                     newMap.table.put(key.dup(), table.get(key).dup());
359                 }
360         }
361         return newMap;
362     }
363 
build(ThreadContext context, RubyDescriptor descriptor)364     protected List<DynamicMessage> build(ThreadContext context, RubyDescriptor descriptor) {
365         List<DynamicMessage> list = new ArrayList<DynamicMessage>();
366         RubyClass rubyClass = (RubyClass) descriptor.msgclass(context);
367         Descriptors.FieldDescriptor keyField = descriptor.lookup("key").getFieldDef();
368         Descriptors.FieldDescriptor valueField = descriptor.lookup("value").getFieldDef();
369         for (IRubyObject key : table.keySet()) {
370             RubyMessage mapMessage = (RubyMessage) rubyClass.newInstance(context, Block.NULL_BLOCK);
371             mapMessage.setField(context, keyField, key);
372             mapMessage.setField(context, valueField, table.get(key));
373             list.add(mapMessage.build(context));
374         }
375         return list;
376     }
377 
mergeIntoSelf(final ThreadContext context, IRubyObject hashmap)378     protected RubyMap mergeIntoSelf(final ThreadContext context, IRubyObject hashmap) {
379         if (hashmap instanceof RubyHash) {
380             ((RubyHash) hashmap).visitAll(new RubyHash.Visitor() {
381                 @Override
382                 public void visit(IRubyObject key, IRubyObject val) {
383                     indexSet(context, key, val);
384                 }
385             });
386         } else if (hashmap instanceof RubyMap) {
387             RubyMap other = (RubyMap) hashmap;
388             if (!typeCompatible(other)) {
389                 throw context.runtime.newTypeError("Attempt to merge Map with mismatching types");
390             }
391         } else {
392             throw context.runtime.newTypeError("Unknown type merging into Map");
393         }
394         return this;
395     }
396 
typeCompatible(RubyMap other)397     protected boolean typeCompatible(RubyMap other) {
398         return this.keyType == other.keyType &&
399                 this.valueType == other.valueType &&
400                 this.valueTypeClass == other.valueTypeClass;
401     }
402 
newThisType(ThreadContext context)403     private RubyMap newThisType(ThreadContext context) {
404         RubyMap newMap;
405         if (needTypeclass(valueType)) {
406             newMap = (RubyMap) metaClass.newInstance(context,
407                     Utils.fieldTypeToRuby(context, keyType),
408                     Utils.fieldTypeToRuby(context, valueType),
409                     valueTypeClass, Block.NULL_BLOCK);
410         } else {
411             newMap = (RubyMap) metaClass.newInstance(context,
412                     Utils.fieldTypeToRuby(context, keyType),
413                     Utils.fieldTypeToRuby(context, valueType),
414                     Block.NULL_BLOCK);
415         }
416         newMap.table = new HashMap<IRubyObject, IRubyObject>();
417         return newMap;
418     }
419 
needTypeclass(Descriptors.FieldDescriptor.Type type)420     private boolean needTypeclass(Descriptors.FieldDescriptor.Type type) {
421         switch(type) {
422             case MESSAGE:
423             case ENUM:
424                 return true;
425             default:
426                 return false;
427         }
428     }
429 
430     private Descriptors.FieldDescriptor.Type keyType;
431     private Descriptors.FieldDescriptor.Type valueType;
432     private IRubyObject valueTypeClass;
433     private Map<IRubyObject, IRubyObject> table;
434 }
435