Index: third_party/protobuf/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java |
diff --git a/third_party/protobuf/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java b/third_party/protobuf/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2d4c03b567ea70b6a9f3681bf4df24ede01f6f51 |
--- /dev/null |
+++ b/third_party/protobuf/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java |
@@ -0,0 +1,434 @@ |
+/* |
+ * 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", "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; |
+} |