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