/* * 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) { key = Utils.checkType(context, keyType, key, (RubyModule) valueTypeClass); value = 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", "to_hash"}) 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; }