• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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