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 org.jruby.*; 37 import org.jruby.anno.JRubyClass; 38 import org.jruby.anno.JRubyMethod; 39 import org.jruby.runtime.Block; 40 import org.jruby.runtime.ObjectAllocator; 41 import org.jruby.runtime.ThreadContext; 42 import org.jruby.runtime.builtin.IRubyObject; 43 44 @JRubyClass(name = "RepeatedClass", include = "Enumerable") 45 public class RubyRepeatedField extends RubyObject { createRubyRepeatedField(Ruby runtime)46 public static void createRubyRepeatedField(Ruby runtime) { 47 RubyModule mProtobuf = runtime.getClassFromPath("Google::Protobuf"); 48 RubyClass cRepeatedField = 49 mProtobuf.defineClassUnder( 50 "RepeatedField", 51 runtime.getObject(), 52 new ObjectAllocator() { 53 @Override 54 public IRubyObject allocate(Ruby runtime, RubyClass klazz) { 55 return new RubyRepeatedField(runtime, klazz); 56 } 57 }); 58 cRepeatedField.defineAnnotatedMethods(RubyRepeatedField.class); 59 cRepeatedField.includeModule(runtime.getEnumerable()); 60 } 61 RubyRepeatedField(Ruby runtime, RubyClass klazz)62 public RubyRepeatedField(Ruby runtime, RubyClass klazz) { 63 super(runtime, klazz); 64 } 65 RubyRepeatedField( Ruby runtime, RubyClass klazz, FieldDescriptor.Type fieldType, IRubyObject typeClass)66 public RubyRepeatedField( 67 Ruby runtime, RubyClass klazz, FieldDescriptor.Type fieldType, IRubyObject typeClass) { 68 this(runtime, klazz); 69 this.fieldType = fieldType; 70 this.storage = runtime.newArray(); 71 this.typeClass = typeClass; 72 } 73 74 @JRubyMethod(required = 1, optional = 2) initialize(ThreadContext context, IRubyObject[] args)75 public IRubyObject initialize(ThreadContext context, IRubyObject[] args) { 76 Ruby runtime = context.runtime; 77 // Workaround for https://github.com/jruby/jruby/issues/7851. Can be removed when JRuby 9.4.3.0 78 // is no longer supported. 79 if (args.length < 1) throw runtime.newArgumentError("Expected at least 1 argument"); 80 this.storage = runtime.newArray(); 81 IRubyObject ary = null; 82 if (!(args[0] instanceof RubySymbol)) { 83 throw runtime.newArgumentError("Expected Symbol for type name"); 84 } 85 this.fieldType = Utils.rubyToFieldType(args[0]); 86 if (fieldType == FieldDescriptor.Type.MESSAGE || fieldType == FieldDescriptor.Type.ENUM) { 87 if (args.length < 2) 88 throw runtime.newArgumentError("Expected at least 2 arguments for message/enum"); 89 typeClass = args[1]; 90 if (args.length > 2) ary = args[2]; 91 Utils.validateTypeClass(context, fieldType, typeClass); 92 } else { 93 if (args.length > 2) throw runtime.newArgumentError("Too many arguments: expected 1 or 2"); 94 if (args.length > 1) ary = args[1]; 95 } 96 if (ary != null) { 97 RubyArray arr = ary.convertToArray(); 98 for (int i = 0; i < arr.size(); i++) { 99 this.storage.add(arr.eltInternal(i)); 100 } 101 } 102 return this; 103 } 104 105 /* 106 * call-seq: 107 * RepeatedField.[]=(index, value) 108 * 109 * Sets the element at the given index. On out-of-bounds assignments, extends 110 * the array and fills the hole (if any) with default values. 111 */ 112 @JRubyMethod(name = "[]=") indexSet(ThreadContext context, IRubyObject index, IRubyObject value)113 public IRubyObject indexSet(ThreadContext context, IRubyObject index, IRubyObject value) { 114 testFrozen("Can't set index in frozen repeated field"); 115 int arrIndex = normalizeArrayIndex(index); 116 value = Utils.checkType(context, fieldType, name, value, (RubyModule) typeClass); 117 IRubyObject defaultValue = defaultValue(context); 118 for (int i = this.storage.size(); i < arrIndex; i++) { 119 this.storage.set(i, defaultValue); 120 } 121 this.storage.set(arrIndex, value); 122 return context.runtime.getNil(); 123 } 124 125 /* 126 * call-seq: 127 * RepeatedField.[](index) => value 128 * 129 * Accesses the element at the given index. Returns nil on out-of-bounds 130 */ 131 @JRubyMethod( 132 required = 1, 133 optional = 1, 134 name = {"at", "[]"}) index(ThreadContext context, IRubyObject[] args)135 public IRubyObject index(ThreadContext context, IRubyObject[] args) { 136 if (args.length == 1) { 137 IRubyObject arg = args[0]; 138 if (Utils.isRubyNum(arg)) { 139 /* standard case */ 140 int arrIndex = normalizeArrayIndex(arg); 141 if (arrIndex < 0 || arrIndex >= this.storage.size()) { 142 return context.runtime.getNil(); 143 } 144 return this.storage.eltInternal(arrIndex); 145 } else if (arg instanceof RubyRange) { 146 RubyRange range = ((RubyRange) arg); 147 148 boolean beginless = range.begin(context).isNil(); 149 int first = 150 normalizeArrayIndex( 151 beginless ? RubyNumeric.int2fix(context.runtime, 0) : range.begin(context)); 152 boolean endless = range.end(context).isNil(); 153 int last = 154 normalizeArrayIndex( 155 endless ? RubyNumeric.int2fix(context.runtime, -1) : range.end(context)); 156 157 if (last - first < 0) { 158 return context.runtime.newEmptyArray(); 159 } 160 boolean excludeEnd = range.isExcludeEnd() && !endless; 161 return this.storage.subseq(first, last - first + (excludeEnd ? 0 : 1)); 162 } 163 } 164 /* assume 2 arguments */ 165 int beg = RubyNumeric.num2int(args[0]); 166 int len = RubyNumeric.num2int(args[1]); 167 if (beg < 0) { 168 beg += this.storage.size(); 169 } 170 if (beg >= this.storage.size()) { 171 return context.runtime.getNil(); 172 } 173 return this.storage.subseq(beg, len); 174 } 175 176 /* 177 * call-seq: 178 * RepeatedField.push(value) 179 * 180 * Adds a new element to the repeated field. 181 */ 182 @JRubyMethod( 183 name = {"push", "<<"}, 184 required = 1, 185 rest = true) push(ThreadContext context, IRubyObject[] args)186 public IRubyObject push(ThreadContext context, IRubyObject[] args) { 187 testFrozen("Can't push frozen repeated field"); 188 for (int i = 0; i < args.length; i++) { 189 IRubyObject val = args[i]; 190 if (fieldType != FieldDescriptor.Type.MESSAGE || !val.isNil()) { 191 val = Utils.checkType(context, fieldType, name, val, (RubyModule) typeClass); 192 } 193 storage.add(val); 194 } 195 196 return this; 197 } 198 199 /* 200 * private Ruby method used by RepeatedField.pop 201 */ 202 @JRubyMethod(visibility = org.jruby.runtime.Visibility.PRIVATE) pop_one(ThreadContext context)203 public IRubyObject pop_one(ThreadContext context) { 204 testFrozen("Can't pop frozen repeated field"); 205 IRubyObject ret = this.storage.last(); 206 this.storage.remove(ret); 207 return ret; 208 } 209 210 /* 211 * call-seq: 212 * RepeatedField.replace(list) 213 * 214 * Replaces the contents of the repeated field with the given list of elements. 215 */ 216 @JRubyMethod replace(ThreadContext context, IRubyObject list)217 public IRubyObject replace(ThreadContext context, IRubyObject list) { 218 testFrozen("Can't replace frozen repeated field"); 219 RubyArray arr = (RubyArray) list; 220 checkArrayElementType(context, arr); 221 this.storage = arr; 222 return this; 223 } 224 225 /* 226 * call-seq: 227 * RepeatedField.clear 228 * 229 * Clears (removes all elements from) this repeated field. 230 */ 231 @JRubyMethod clear(ThreadContext context)232 public IRubyObject clear(ThreadContext context) { 233 testFrozen("Can't clear frozen repeated field"); 234 this.storage.clear(); 235 return this; 236 } 237 238 /* 239 * call-seq: 240 * RepeatedField.length 241 * 242 * Returns the length of this repeated field. 243 */ 244 @JRubyMethod(name = {"length", "size"}) length(ThreadContext context)245 public IRubyObject length(ThreadContext context) { 246 return context.runtime.newFixnum(this.storage.size()); 247 } 248 249 /* 250 * call-seq: 251 * RepeatedField.+(other) => repeated field 252 * 253 * Returns a new repeated field that contains the concatenated list of this 254 * repeated field's elements and other's elements. The other (second) list may 255 * be either another repeated field or a Ruby array. 256 */ 257 @JRubyMethod(name = {"+"}) plus(ThreadContext context, IRubyObject list)258 public IRubyObject plus(ThreadContext context, IRubyObject list) { 259 RubyRepeatedField dup = (RubyRepeatedField) dup(context); 260 if (list instanceof RubyArray) { 261 checkArrayElementType(context, (RubyArray) list); 262 dup.storage.addAll((RubyArray) list); 263 } else { 264 RubyRepeatedField repeatedField = (RubyRepeatedField) list; 265 if (!fieldType.equals(repeatedField.fieldType) 266 || (typeClass != null && !typeClass.equals(repeatedField.typeClass))) 267 throw context.runtime.newArgumentError( 268 "Attempt to append RepeatedField with different element type."); 269 dup.storage.addAll((RubyArray) repeatedField.toArray(context)); 270 } 271 return dup; 272 } 273 274 /* 275 * call-seq: 276 * RepeatedField.concat(other) => self 277 * 278 * concats the passed in array to self. Returns a Ruby array. 279 */ 280 @JRubyMethod concat(ThreadContext context, IRubyObject list)281 public IRubyObject concat(ThreadContext context, IRubyObject list) { 282 testFrozen("Can't concat frozen repeated field"); 283 if (list instanceof RubyArray) { 284 checkArrayElementType(context, (RubyArray) list); 285 this.storage.addAll((RubyArray) list); 286 } else { 287 RubyRepeatedField repeatedField = (RubyRepeatedField) list; 288 if (!fieldType.equals(repeatedField.fieldType) 289 || (typeClass != null && !typeClass.equals(repeatedField.typeClass))) 290 throw context.runtime.newArgumentError( 291 "Attempt to append RepeatedField with different element type."); 292 this.storage.addAll((RubyArray) repeatedField.toArray(context)); 293 } 294 return this; 295 } 296 297 /* 298 * call-seq: 299 * RepeatedField.hash => hash_value 300 * 301 * Returns a hash value computed from this repeated field's elements. 302 */ 303 @JRubyMethod hash(ThreadContext context)304 public IRubyObject hash(ThreadContext context) { 305 int hashCode = this.storage.hashCode(); 306 return context.runtime.newFixnum(hashCode); 307 } 308 309 /* 310 * call-seq: 311 * RepeatedField.==(other) => boolean 312 * 313 * Compares this repeated field to another. Repeated fields are equal if their 314 * element types are equal, their lengths are equal, and each element is equal. 315 * Elements are compared as per normal Ruby semantics, by calling their :== 316 * methods (or performing a more efficient comparison for primitive types). 317 */ 318 @JRubyMethod(name = "==") eq(ThreadContext context, IRubyObject other)319 public IRubyObject eq(ThreadContext context, IRubyObject other) { 320 return this.toArray(context).op_equal(context, other); 321 } 322 323 /* 324 * call-seq: 325 * RepeatedField.each(&block) 326 * 327 * Invokes the block once for each element of the repeated field. RepeatedField 328 * also includes Enumerable; combined with this method, the repeated field thus 329 * acts like an ordinary Ruby sequence. 330 */ 331 @JRubyMethod each(ThreadContext context, Block block)332 public IRubyObject each(ThreadContext context, Block block) { 333 this.storage.each(context, block); 334 return this; 335 } 336 337 @JRubyMethod(name = {"to_ary", "to_a"}) toArray(ThreadContext context)338 public IRubyObject toArray(ThreadContext context) { 339 return this.storage; 340 } 341 342 /* 343 * call-seq: 344 * RepeatedField.dup => repeated_field 345 * 346 * Duplicates this repeated field with a shallow copy. References to all 347 * non-primitive element objects (e.g., submessages) are shared. 348 */ 349 @JRubyMethod dup(ThreadContext context)350 public IRubyObject dup(ThreadContext context) { 351 RubyRepeatedField dup = new RubyRepeatedField(context.runtime, metaClass, fieldType, typeClass); 352 dup.push(context, storage.toJavaArray()); 353 return dup; 354 } 355 356 @JRubyMethod inspect()357 public IRubyObject inspect() { 358 return storage.inspect(); 359 } 360 361 @JRubyMethod freeze(ThreadContext context)362 public IRubyObject freeze(ThreadContext context) { 363 if (isFrozen()) { 364 return this; 365 } 366 setFrozen(true); 367 if (fieldType == FieldDescriptor.Type.MESSAGE) { 368 for (int i = 0; i < size(); i++) { 369 ((RubyMessage) storage.eltInternal(i)).freeze(context); 370 } 371 } 372 return this; 373 } 374 375 // Java API get(int index)376 protected IRubyObject get(int index) { 377 return this.storage.eltInternal(index); 378 } 379 deepCopy(ThreadContext context)380 protected RubyRepeatedField deepCopy(ThreadContext context) { 381 RubyRepeatedField copy = 382 new RubyRepeatedField(context.runtime, metaClass, fieldType, typeClass); 383 for (int i = 0; i < size(); i++) { 384 IRubyObject value = storage.eltInternal(i); 385 if (fieldType == FieldDescriptor.Type.MESSAGE) { 386 copy.storage.add(((RubyMessage) value).deepCopy(context)); 387 } else { 388 copy.storage.add(value); 389 } 390 } 391 return copy; 392 } 393 setName(String name)394 protected void setName(String name) { 395 this.name = name; 396 } 397 size()398 protected int size() { 399 return this.storage.size(); 400 } 401 defaultValue(ThreadContext context)402 private IRubyObject defaultValue(ThreadContext context) { 403 Object value; 404 switch (fieldType) { 405 case INT32: 406 case UINT32: 407 value = 0; 408 break; 409 case INT64: 410 case UINT64: 411 value = 0L; 412 break; 413 case FLOAT: 414 value = 0F; 415 break; 416 case DOUBLE: 417 value = 0D; 418 break; 419 case BOOL: 420 value = false; 421 break; 422 case BYTES: 423 value = com.google.protobuf.ByteString.EMPTY; 424 break; 425 case STRING: 426 value = ""; 427 break; 428 case ENUM: 429 IRubyObject defaultEnumLoc = context.runtime.newFixnum(0); 430 return RubyEnum.lookup(context, typeClass, defaultEnumLoc); 431 default: 432 return context.runtime.getNil(); 433 } 434 return Utils.wrapPrimaryValue(context, fieldType, value); 435 } 436 checkArrayElementType(ThreadContext context, RubyArray arr)437 private void checkArrayElementType(ThreadContext context, RubyArray arr) { 438 for (int i = 0; i < arr.getLength(); i++) { 439 Utils.checkType(context, fieldType, name, arr.eltInternal(i), (RubyModule) typeClass); 440 } 441 } 442 normalizeArrayIndex(IRubyObject index)443 private int normalizeArrayIndex(IRubyObject index) { 444 int arrIndex = RubyNumeric.num2int(index); 445 int arrSize = this.storage.size(); 446 if (arrIndex < 0 && arrSize > 0) { 447 arrIndex = arrSize + arrIndex; 448 } 449 return arrIndex; 450 } 451 452 private FieldDescriptor.Type fieldType; 453 private IRubyObject typeClass; 454 private RubyArray storage; 455 private String name; 456 } 457