| /* |
| * Protocol Buffers - Google's data interchange format |
| * Copyright 2014 Google Inc. All rights reserved. |
| * https://developers.google.com/protocol-buffers/ |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| package com.google.protobuf.jruby; |
| |
| import com.google.protobuf.Descriptors; |
| import com.google.protobuf.DynamicMessage; |
| import com.google.protobuf.MapEntry; |
| import org.jruby.*; |
| import org.jruby.anno.JRubyClass; |
| import org.jruby.anno.JRubyMethod; |
| import org.jruby.internal.runtime.methods.DynamicMethod; |
| import org.jruby.runtime.Block; |
| import org.jruby.runtime.ObjectAllocator; |
| import org.jruby.runtime.ThreadContext; |
| import org.jruby.runtime.builtin.IRubyObject; |
| import org.jruby.util.ByteList; |
| |
| import java.security.MessageDigest; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| @JRubyClass(name = "Map", include = "Enumerable") |
| public class RubyMap extends RubyObject { |
| public static void createRubyMap(Ruby runtime) { |
| RubyModule protobuf = runtime.getClassFromPath("Google::Protobuf"); |
| RubyClass cMap = protobuf.defineClassUnder("Map", runtime.getObject(), new ObjectAllocator() { |
| @Override |
| public IRubyObject allocate(Ruby ruby, RubyClass rubyClass) { |
| return new RubyMap(ruby, rubyClass); |
| } |
| }); |
| cMap.includeModule(runtime.getEnumerable()); |
| cMap.defineAnnotatedMethods(RubyMap.class); |
| } |
| |
| public RubyMap(Ruby ruby, RubyClass rubyClass) { |
| super(ruby, rubyClass); |
| } |
| |
| /* |
| * call-seq: |
| * Map.new(key_type, value_type, value_typeclass = nil, init_hashmap = {}) |
| * => new map |
| * |
| * Allocates a new Map container. This constructor may be called with 2, 3, or 4 |
| * arguments. The first two arguments are always present and are symbols (taking |
| * on the same values as field-type symbols in message descriptors) that |
| * indicate the type of the map key and value fields. |
| * |
| * The supported key types are: :int32, :int64, :uint32, :uint64, :bool, |
| * :string, :bytes. |
| * |
| * The supported value types are: :int32, :int64, :uint32, :uint64, :bool, |
| * :string, :bytes, :enum, :message. |
| * |
| * The third argument, value_typeclass, must be present if value_type is :enum |
| * or :message. As in RepeatedField#new, this argument must be a message class |
| * (for :message) or enum module (for :enum). |
| * |
| * The last argument, if present, provides initial content for map. Note that |
| * this may be an ordinary Ruby hashmap or another Map instance with identical |
| * key and value types. Also note that this argument may be present whether or |
| * not value_typeclass is present (and it is unambiguously separate from |
| * value_typeclass because value_typeclass's presence is strictly determined by |
| * value_type). The contents of this initial hashmap or Map instance are |
| * shallow-copied into the new Map: the original map is unmodified, but |
| * references to underlying objects will be shared if the value type is a |
| * message type. |
| */ |
| |
| @JRubyMethod(required = 2, optional = 2) |
| public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { |
| this.table = new HashMap<IRubyObject, IRubyObject>(); |
| this.keyType = Utils.rubyToFieldType(args[0]); |
| this.valueType = Utils.rubyToFieldType(args[1]); |
| |
| switch(keyType) { |
| case INT32: |
| case INT64: |
| case UINT32: |
| case UINT64: |
| case BOOL: |
| case STRING: |
| case BYTES: |
| // These are OK. |
| break; |
| default: |
| throw context.runtime.newArgumentError("Invalid key type for map."); |
| } |
| |
| int initValueArg = 2; |
| if (needTypeclass(this.valueType) && args.length > 2) { |
| this.valueTypeClass = args[2]; |
| Utils.validateTypeClass(context, this.valueType, this.valueTypeClass); |
| initValueArg = 3; |
| } else { |
| this.valueTypeClass = context.runtime.getNilClass(); |
| } |
| |
| // Table value type is always UINT64: this ensures enough space to store the |
| // native_slot value. |
| if (args.length > initValueArg) { |
| mergeIntoSelf(context, args[initValueArg]); |
| } |
| return this; |
| } |
| |
| /* |
| * call-seq: |
| * Map.[]=(key, value) => value |
| * |
| * Inserts or overwrites the value at the given key with the given new value. |
| * Throws an exception if the key type is incorrect. Returns the new value that |
| * was just inserted. |
| */ |
| @JRubyMethod(name = "[]=") |
| public IRubyObject indexSet(ThreadContext context, IRubyObject key, IRubyObject value) { |
| Utils.checkType(context, keyType, key, (RubyModule) valueTypeClass); |
| Utils.checkType(context, valueType, value, (RubyModule) valueTypeClass); |
| IRubyObject symbol; |
| if (valueType == Descriptors.FieldDescriptor.Type.ENUM && |
| Utils.isRubyNum(value) && |
| ! (symbol = RubyEnum.lookup(context, valueTypeClass, value)).isNil()) { |
| value = symbol; |
| } |
| this.table.put(key, value); |
| return value; |
| } |
| |
| /* |
| * call-seq: |
| * Map.[](key) => value |
| * |
| * Accesses the element at the given key. Throws an exception if the key type is |
| * incorrect. Returns nil when the key is not present in the map. |
| */ |
| @JRubyMethod(name = "[]") |
| public IRubyObject index(ThreadContext context, IRubyObject key) { |
| if (table.containsKey(key)) |
| return this.table.get(key); |
| return context.runtime.getNil(); |
| } |
| |
| /* |
| * call-seq: |
| * Map.==(other) => boolean |
| * |
| * Compares this map to another. Maps are equal if they have identical key sets, |
| * and for each key, the values in both maps compare equal. Elements are |
| * compared as per normal Ruby semantics, by calling their :== methods (or |
| * performing a more efficient comparison for primitive types). |
| * |
| * Maps with dissimilar key types or value types/typeclasses are never equal, |
| * even if value comparison (for example, between integers and floats) would |
| * have otherwise indicated that every element has equal value. |
| */ |
| @JRubyMethod(name = "==") |
| public IRubyObject eq(ThreadContext context, IRubyObject _other) { |
| if (_other instanceof RubyHash) |
| return toHash(context).op_equal(context, _other); |
| RubyMap other = (RubyMap) _other; |
| if (this == other) return context.runtime.getTrue(); |
| if (!typeCompatible(other) || this.table.size() != other.table.size()) |
| return context.runtime.getFalse(); |
| for (IRubyObject key : table.keySet()) { |
| if (! other.table.containsKey(key)) |
| return context.runtime.getFalse(); |
| if (! other.table.get(key).equals(table.get(key))) |
| return context.runtime.getFalse(); |
| } |
| return context.runtime.getTrue(); |
| } |
| |
| /* |
| * call-seq: |
| * Map.inspect => string |
| * |
| * Returns a string representing this map's elements. It will be formatted as |
| * "{key => value, key => value, ...}", with each key and value string |
| * representation computed by its own #inspect method. |
| */ |
| @JRubyMethod |
| public IRubyObject inspect() { |
| return toHash(getRuntime().getCurrentContext()).inspect(); |
| } |
| |
| /* |
| * call-seq: |
| * Map.hash => hash_value |
| * |
| * Returns a hash value based on this map's contents. |
| */ |
| @JRubyMethod |
| public IRubyObject hash(ThreadContext context) { |
| try { |
| MessageDigest digest = MessageDigest.getInstance("SHA-256"); |
| for (IRubyObject key : table.keySet()) { |
| digest.update((byte) key.hashCode()); |
| digest.update((byte) table.get(key).hashCode()); |
| } |
| return context.runtime.newString(new ByteList(digest.digest())); |
| } catch (NoSuchAlgorithmException ignore) { |
| return context.runtime.newFixnum(System.identityHashCode(table)); |
| } |
| } |
| |
| /* |
| * call-seq: |
| * Map.keys => [list_of_keys] |
| * |
| * Returns the list of keys contained in the map, in unspecified order. |
| */ |
| @JRubyMethod |
| public IRubyObject keys(ThreadContext context) { |
| return RubyArray.newArray(context.runtime, table.keySet()); |
| } |
| |
| /* |
| * call-seq: |
| * Map.values => [list_of_values] |
| * |
| * Returns the list of values contained in the map, in unspecified order. |
| */ |
| @JRubyMethod |
| public IRubyObject values(ThreadContext context) { |
| return RubyArray.newArray(context.runtime, table.values()); |
| } |
| |
| /* |
| * call-seq: |
| * Map.clear |
| * |
| * Removes all entries from the map. |
| */ |
| @JRubyMethod |
| public IRubyObject clear(ThreadContext context) { |
| table.clear(); |
| return context.runtime.getNil(); |
| } |
| |
| /* |
| * call-seq: |
| * Map.each(&block) |
| * |
| * Invokes &block on each |key, value| pair in the map, in unspecified order. |
| * Note that Map also includes Enumerable; map thus acts like a normal Ruby |
| * sequence. |
| */ |
| @JRubyMethod |
| public IRubyObject each(ThreadContext context, Block block) { |
| for (IRubyObject key : table.keySet()) { |
| block.yieldSpecific(context, key, table.get(key)); |
| } |
| return context.runtime.getNil(); |
| } |
| |
| /* |
| * call-seq: |
| * Map.delete(key) => old_value |
| * |
| * Deletes the value at the given key, if any, returning either the old value or |
| * nil if none was present. Throws an exception if the key is of the wrong type. |
| */ |
| @JRubyMethod |
| public IRubyObject delete(ThreadContext context, IRubyObject key) { |
| return table.remove(key); |
| } |
| |
| /* |
| * call-seq: |
| * Map.has_key?(key) => bool |
| * |
| * Returns true if the given key is present in the map. Throws an exception if |
| * the key has the wrong type. |
| */ |
| @JRubyMethod(name = "has_key?") |
| public IRubyObject hasKey(ThreadContext context, IRubyObject key) { |
| return this.table.containsKey(key) ? context.runtime.getTrue() : context.runtime.getFalse(); |
| } |
| |
| /* |
| * call-seq: |
| * Map.length |
| * |
| * Returns the number of entries (key-value pairs) in the map. |
| */ |
| @JRubyMethod |
| public IRubyObject length(ThreadContext context) { |
| return context.runtime.newFixnum(this.table.size()); |
| } |
| |
| /* |
| * call-seq: |
| * Map.dup => new_map |
| * |
| * Duplicates this map with a shallow copy. References to all non-primitive |
| * element objects (e.g., submessages) are shared. |
| */ |
| @JRubyMethod |
| public IRubyObject dup(ThreadContext context) { |
| RubyMap newMap = newThisType(context); |
| for (Map.Entry<IRubyObject, IRubyObject> entry : table.entrySet()) { |
| newMap.table.put(entry.getKey(), entry.getValue()); |
| } |
| return newMap; |
| } |
| |
| @JRubyMethod(name = "to_h") |
| public RubyHash toHash(ThreadContext context) { |
| return RubyHash.newHash(context.runtime, table, context.runtime.getNil()); |
| } |
| |
| // Used by Google::Protobuf.deep_copy but not exposed directly. |
| protected IRubyObject deepCopy(ThreadContext context) { |
| RubyMap newMap = newThisType(context); |
| switch (valueType) { |
| case MESSAGE: |
| for (IRubyObject key : table.keySet()) { |
| RubyMessage message = (RubyMessage) table.get(key); |
| newMap.table.put(key.dup(), message.deepCopy(context)); |
| } |
| break; |
| default: |
| for (IRubyObject key : table.keySet()) { |
| newMap.table.put(key.dup(), table.get(key).dup()); |
| } |
| } |
| return newMap; |
| } |
| |
| protected List<DynamicMessage> build(ThreadContext context, RubyDescriptor descriptor) { |
| List<DynamicMessage> list = new ArrayList<DynamicMessage>(); |
| RubyClass rubyClass = (RubyClass) descriptor.msgclass(context); |
| Descriptors.FieldDescriptor keyField = descriptor.lookup("key").getFieldDef(); |
| Descriptors.FieldDescriptor valueField = descriptor.lookup("value").getFieldDef(); |
| for (IRubyObject key : table.keySet()) { |
| RubyMessage mapMessage = (RubyMessage) rubyClass.newInstance(context, Block.NULL_BLOCK); |
| mapMessage.setField(context, keyField, key); |
| mapMessage.setField(context, valueField, table.get(key)); |
| list.add(mapMessage.build(context)); |
| } |
| return list; |
| } |
| |
| protected RubyMap mergeIntoSelf(final ThreadContext context, IRubyObject hashmap) { |
| if (hashmap instanceof RubyHash) { |
| ((RubyHash) hashmap).visitAll(new RubyHash.Visitor() { |
| @Override |
| public void visit(IRubyObject key, IRubyObject val) { |
| indexSet(context, key, val); |
| } |
| }); |
| } else if (hashmap instanceof RubyMap) { |
| RubyMap other = (RubyMap) hashmap; |
| if (!typeCompatible(other)) { |
| throw context.runtime.newTypeError("Attempt to merge Map with mismatching types"); |
| } |
| } else { |
| throw context.runtime.newTypeError("Unknown type merging into Map"); |
| } |
| return this; |
| } |
| |
| protected boolean typeCompatible(RubyMap other) { |
| return this.keyType == other.keyType && |
| this.valueType == other.valueType && |
| this.valueTypeClass == other.valueTypeClass; |
| } |
| |
| private RubyMap newThisType(ThreadContext context) { |
| RubyMap newMap; |
| if (needTypeclass(valueType)) { |
| newMap = (RubyMap) metaClass.newInstance(context, |
| Utils.fieldTypeToRuby(context, keyType), |
| Utils.fieldTypeToRuby(context, valueType), |
| valueTypeClass, Block.NULL_BLOCK); |
| } else { |
| newMap = (RubyMap) metaClass.newInstance(context, |
| Utils.fieldTypeToRuby(context, keyType), |
| Utils.fieldTypeToRuby(context, valueType), |
| Block.NULL_BLOCK); |
| } |
| newMap.table = new HashMap<IRubyObject, IRubyObject>(); |
| return newMap; |
| } |
| |
| private boolean needTypeclass(Descriptors.FieldDescriptor.Type type) { |
| switch(type) { |
| case MESSAGE: |
| case ENUM: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| private Descriptors.FieldDescriptor.Type keyType; |
| private Descriptors.FieldDescriptor.Type valueType; |
| private IRubyObject valueTypeClass; |
| private Map<IRubyObject, IRubyObject> table; |
| } |