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