Home | History | Annotate | Download | only in jruby
      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 {
     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 
     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)
    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 = "[]=")
    150     public IRubyObject indexSet(ThreadContext context, IRubyObject key, IRubyObject value) {
    151         Utils.checkType(context, keyType, key, (RubyModule) valueTypeClass);
    152         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 = "[]")
    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 = "==")
    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
    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
    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
    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
    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
    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
    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
    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?")
    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
    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
    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", "to_hash"})
    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.
    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 
    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 
    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 
    397     protected boolean typeCompatible(RubyMap other) {
    398         return this.keyType == other.keyType &&
    399                 this.valueType == other.valueType &&
    400                 this.valueTypeClass == other.valueTypeClass;
    401     }
    402 
    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 
    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