diff options
Diffstat (limited to 'java')
6 files changed, 497 insertions, 4 deletions
diff --git a/java/src/main/java/com/google/protobuf/nano/CodedInputByteBufferNano.java b/java/src/main/java/com/google/protobuf/nano/CodedInputByteBufferNano.java index ed387882..c5fea5ae 100644 --- a/java/src/main/java/com/google/protobuf/nano/CodedInputByteBufferNano.java +++ b/java/src/main/java/com/google/protobuf/nano/CodedInputByteBufferNano.java @@ -541,6 +541,23 @@ public final class CodedInputByteBufferNano { } /** + * Retrieves a subset of data in the buffer. The returned array is not backed by the original + * buffer array. + * + * @param offset the position (relative to the buffer start position) to start at. + * @param length the number of bytes to retrieve. + */ + public byte[] getData(int offset, int length) { + if (length == 0) { + return WireFormatNano.EMPTY_BYTES; + } + byte[] copy = new byte[length]; + int start = bufferStart + offset; + System.arraycopy(buffer, start, copy, 0, length); + return copy; + } + + /** * Rewind to previous position. Cannot go forward. */ public void rewindToPosition(int position) { diff --git a/java/src/main/java/com/google/protobuf/nano/Extension.java b/java/src/main/java/com/google/protobuf/nano/Extension.java new file mode 100644 index 00000000..4512b01a --- /dev/null +++ b/java/src/main/java/com/google/protobuf/nano/Extension.java @@ -0,0 +1,114 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2013 Google Inc. All rights reserved. +// http://code.google.com/p/protobuf/ +// +// 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.nano; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; + +/** + * Represents an extension. + * + * @author bduff@google.com (Brian Duff) + * @param <T> the type of the extension. + */ +public class Extension<T> { + public final int fieldNumber; + public boolean isRepeatedField; + public Class<T> fieldType; + public Class<T> listType; + + private Extension(int fieldNumber, TypeLiteral<T> type) { + this.fieldNumber = fieldNumber; + isRepeatedField = type.isList(); + fieldType = type.getTargetClass(); + listType = isRepeatedField ? type.getListType() : null; + } + + /** + * Creates a new instance of {@code Extension} for the specified {@code fieldNumber} and + * {@code type}. + */ + public static <T> Extension<T> create(int fieldNumber, TypeLiteral<T> type) { + return new Extension<T>(fieldNumber, type); + } + + /** + * Creates a new instance of {@code Extension} for the specified {@code fieldNumber} and + * {@code type}. This version is used for repeated fields. + */ + public static <T> Extension<List<T>> createRepeated(int fieldNumber, TypeLiteral<List<T>> type) { + return new Extension<List<T>>(fieldNumber, type); + } + + /** + * Represents a generic type literal. We can't typesafely reference a + * Class<List<Foo>>.class in Java, so we use this instead. + * See: http://gafter.blogspot.com/2006/12/super-type-tokens.html + * + * <p>Somewhat specialized because we only ever have a Foo or a List<Foo>. + */ + public static abstract class TypeLiteral<T> { + private final Type type; + + protected TypeLiteral() { + Type superclass = getClass().getGenericSuperclass(); + if (superclass instanceof Class) { + throw new RuntimeException("Missing type parameter"); + } + this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0]; + } + + /** + * If the generic type is a list, returns {@code true}. + */ + private boolean isList() { + return type instanceof ParameterizedType; + } + + @SuppressWarnings("unchecked") + private Class<T> getListType() { + return (Class<T>) ((ParameterizedType) type).getRawType(); + } + + /** + * If the generic type is a list, returns the type of element in the list. Otherwise, + * returns the actual type. + */ + @SuppressWarnings("unchecked") + private Class<T> getTargetClass() { + if (isList()) { + return (Class<T>) ((ParameterizedType) type).getActualTypeArguments()[0]; + } + return (Class<T>) type; + } + } +} diff --git a/java/src/main/java/com/google/protobuf/nano/MessageNano.java b/java/src/main/java/com/google/protobuf/nano/MessageNano.java index d6c1e9a8..5c1eb2ff 100644 --- a/java/src/main/java/com/google/protobuf/nano/MessageNano.java +++ b/java/src/main/java/com/google/protobuf/nano/MessageNano.java @@ -93,7 +93,7 @@ public abstract class MessageNano { output.checkNoSpaceLeft(); } catch (IOException e) { throw new RuntimeException("Serializing to a byte array threw an IOException " - + "(should never happen)."); + + "(should never happen).", e); } } diff --git a/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java b/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java new file mode 100644 index 00000000..0db2a83c --- /dev/null +++ b/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java @@ -0,0 +1,47 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2013 Google Inc. All rights reserved. +// http://code.google.com/p/protobuf/ +// +// 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.nano; + +/** + * Stores unknown fields. These might be extensions or fields that the generated API doesn't + * know about yet. + * + * @author bduff@google.com (Brian Duff) + */ +public final class UnknownFieldData { + final int tag; + final byte[] bytes; + + UnknownFieldData(int tag, byte[] bytes) { + this.tag = tag; + this.bytes = bytes; + } +} diff --git a/java/src/main/java/com/google/protobuf/nano/WireFormatNano.java b/java/src/main/java/com/google/protobuf/nano/WireFormatNano.java index 8fa36364..c901e59c 100644 --- a/java/src/main/java/com/google/protobuf/nano/WireFormatNano.java +++ b/java/src/main/java/com/google/protobuf/nano/WireFormatNano.java @@ -31,6 +31,9 @@ package com.google.protobuf.nano; import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; /** * This class is used internally by the Protocol Buffer library and generated @@ -97,8 +100,12 @@ public final class WireFormatNano { public static final byte[] EMPTY_BYTES = {}; /** - * Called by subclasses to parse an unknown field. - * @return {@code true} unless the tag is an end-group tag. + * Parses an unknown field. This implementation skips the field. + * + * <p>Generated messages will call this for unknown fields if the store_unknown_fields + * option is off. + * + * @return {@literal true} unless the tag is an end-group tag. */ public static boolean parseUnknownField( final CodedInputByteBufferNano input, @@ -107,6 +114,30 @@ public final class WireFormatNano { } /** + * Stores the binary data of an unknown field. + * + * <p>Generated messages will call this for unknown fields if the store_unknown_fields + * option is on. + * + * @param data a Collection in which to store the data. + * @param input the input buffer. + * @param tag the tag of the field. + + * @return {@literal true} unless the tag is an end-group tag. + */ + public static boolean storeUnknownField( + final List<UnknownFieldData> data, + final CodedInputByteBufferNano input, + final int tag) throws IOException { + int startPos = input.getPosition(); + boolean skip = input.skipField(tag); + int endPos = input.getPosition(); + byte[] bytes = input.getData(startPos, endPos - startPos); + data.add(new UnknownFieldData(tag, bytes)); + return skip; + } + + /** * Computes the array length of a repeated field. We assume that in the common case repeated * fields are contiguously serialized but we still correctly handle interspersed values of a * repeated field (but with extra allocations). @@ -135,4 +166,194 @@ public final class WireFormatNano { input.rewindToPosition(startPos); return arrayLength; } + + /** + * Decodes the value of an extension. + */ + public static <T> T getExtension(Extension<T> extension, List<UnknownFieldData> unknownFields) { + if (unknownFields == null) { + return null; + } + List<UnknownFieldData> dataForField = new ArrayList<UnknownFieldData>(); + for (UnknownFieldData data : unknownFields) { + if (getTagFieldNumber(data.tag) == extension.fieldNumber) { + dataForField.add(data); + } + } + if (dataForField.isEmpty()) { + return null; + } + + if (extension.isRepeatedField) { + List<Object> result = new ArrayList<Object>(dataForField.size()); + for (UnknownFieldData data : dataForField) { + result.add(readData(extension.fieldType, data.bytes)); + } + return extension.listType.cast(result); + } + + // Normal fields. Note that the protobuf docs require us to handle multiple instances + // of the same field even for fields that are not repeated. + UnknownFieldData lastData = dataForField.get(dataForField.size() - 1); + return readData(extension.fieldType, lastData.bytes); + } + + /** + * Reads (extension) data of the specified type from the specified byte array. + * + * @throws IllegalArgumentException if an error occurs while reading the data. + */ + private static <T> T readData(Class<T> clazz, byte[] data) { + if (data.length == 0) { + return null; + } + CodedInputByteBufferNano buffer = CodedInputByteBufferNano.newInstance(data); + try { + if (clazz == String.class) { + return clazz.cast(buffer.readString()); + } else if (clazz == Integer.class) { + return clazz.cast(buffer.readInt32()); + } else if (clazz == Long.class) { + return clazz.cast(buffer.readInt64()); + } else if (clazz == Boolean.class) { + return clazz.cast(buffer.readBool()); + } else if (clazz == Float.class) { + return clazz.cast(buffer.readFloat()); + } else if (clazz == Double.class) { + return clazz.cast(buffer.readDouble()); + } else if (clazz == byte[].class) { + return clazz.cast(buffer.readBytes()); + } else if (MessageNano.class.isAssignableFrom(clazz)) { + try { + MessageNano message = (MessageNano) clazz.newInstance(); + buffer.readMessage(message); + return clazz.cast(message); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException("Error creating instance of class " + clazz, e); + } catch (InstantiationException e) { + throw new IllegalArgumentException("Error creating instance of class " + clazz, e); + } + } else { + throw new IllegalArgumentException("Unhandled extension field type: " + clazz); + } + } catch (IOException e) { + throw new IllegalArgumentException("Error reading extension field", e); + } + } + + public static <T> void setExtension(Extension<T> extension, T value, + List<UnknownFieldData> unknownFields) { + // First, remove all unknown fields with this tag. + for (Iterator<UnknownFieldData> i = unknownFields.iterator(); i.hasNext();) { + UnknownFieldData data = i.next(); + if (extension.fieldNumber == getTagFieldNumber(data.tag)) { + i.remove(); + } + } + if (value == null) { + return; + } + // Repeated field. + if (value instanceof List) { + for (Object item : (List<?>) value) { + unknownFields.add(write(extension.fieldNumber, item)); + } + } else { + unknownFields.add(write(extension.fieldNumber, value)); + } + } + + /** + * Writes extension data and returns an {@link UnknownFieldData} containing + * bytes and a tag. + * + * @throws IllegalArgumentException if an error occurs while writing. + */ + private static UnknownFieldData write(int fieldNumber, Object object) { + byte[] data; + int tag; + Class<?> clazz = object.getClass(); + try { + if (clazz == String.class) { + String str = (String) object; + data = new byte[CodedOutputByteBufferNano.computeStringSizeNoTag(str)]; + CodedOutputByteBufferNano.newInstance(data).writeStringNoTag(str); + tag = makeTag(fieldNumber, WIRETYPE_LENGTH_DELIMITED); + } else if (clazz == Integer.class) { + Integer integer = (Integer) object; + data = new byte[CodedOutputByteBufferNano.computeInt32SizeNoTag(integer)]; + CodedOutputByteBufferNano.newInstance(data).writeInt32NoTag(integer); + tag = makeTag(fieldNumber, WIRETYPE_VARINT); + } else if (clazz == Long.class) { + Long longValue = (Long) object; + data = new byte[CodedOutputByteBufferNano.computeInt64SizeNoTag(longValue)]; + CodedOutputByteBufferNano.newInstance(data).writeInt64NoTag(longValue); + tag = makeTag(fieldNumber, WIRETYPE_VARINT); + } else if (clazz == Boolean.class) { + Boolean boolValue = (Boolean) object; + data = new byte[CodedOutputByteBufferNano.computeBoolSizeNoTag(boolValue)]; + CodedOutputByteBufferNano.newInstance(data).writeBoolNoTag(boolValue); + tag = makeTag(fieldNumber, WIRETYPE_VARINT); + } else if (clazz == Float.class) { + Float floatValue = (Float) object; + data = new byte[CodedOutputByteBufferNano.computeFloatSizeNoTag(floatValue)]; + CodedOutputByteBufferNano.newInstance(data).writeFloatNoTag(floatValue); + tag = makeTag(fieldNumber, WIRETYPE_FIXED32); + } else if (clazz == Double.class) { + Double doubleValue = (Double) object; + data = new byte[CodedOutputByteBufferNano.computeDoubleSizeNoTag(doubleValue)]; + CodedOutputByteBufferNano.newInstance(data).writeDoubleNoTag(doubleValue); + tag = makeTag(fieldNumber, WIRETYPE_FIXED64); + } else if (clazz == byte[].class) { + byte[] byteArrayValue = (byte[]) object; + data = new byte[CodedOutputByteBufferNano.computeByteArraySizeNoTag(byteArrayValue)]; + CodedOutputByteBufferNano.newInstance(data).writeByteArrayNoTag(byteArrayValue); + tag = makeTag(fieldNumber, WIRETYPE_LENGTH_DELIMITED); + } else if (MessageNano.class.isAssignableFrom(clazz)) { + MessageNano messageValue = (MessageNano) object; + + int messageSize = messageValue.getSerializedSize(); + int delimiterSize = CodedOutputByteBufferNano.computeRawVarint32Size(messageSize); + data = new byte[messageSize + delimiterSize]; + CodedOutputByteBufferNano buffer = CodedOutputByteBufferNano.newInstance(data); + buffer.writeRawVarint32(messageSize); + buffer.writeRawBytes(MessageNano.toByteArray(messageValue)); + tag = makeTag(fieldNumber, WIRETYPE_LENGTH_DELIMITED); + } else { + throw new IllegalArgumentException("Unhandled extension field type: " + clazz); + } + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + return new UnknownFieldData(tag, data); + } + + /** + * Given a set of unknown field data, compute the wire size. + */ + public static int computeWireSize(List<UnknownFieldData> unknownFields) { + if (unknownFields == null) { + return 0; + } + int size = 0; + for (UnknownFieldData unknownField : unknownFields) { + size += CodedOutputByteBufferNano.computeRawVarint32Size(unknownField.tag); + size += unknownField.bytes.length; + } + return size; + } + + /** + * Write unknown fields. + */ + public static void writeUnknownFields(List<UnknownFieldData> unknownFields, + CodedOutputByteBufferNano outBuffer) throws IOException { + if (unknownFields == null) { + return; + } + for (UnknownFieldData data : unknownFields) { + outBuffer.writeTag(getTagFieldNumber(data.tag), getTagWireType(data.tag)); + outBuffer.writeRawBytes(data.bytes); + } + } } diff --git a/java/src/test/java/com/google/protobuf/NanoTest.java b/java/src/test/java/com/google/protobuf/NanoTest.java index da17a9e1..38fafb9e 100644 --- a/java/src/test/java/com/google/protobuf/NanoTest.java +++ b/java/src/test/java/com/google/protobuf/NanoTest.java @@ -30,6 +30,9 @@ package com.google.protobuf; +import com.google.protobuf.nano.CodedInputByteBufferNano; +import com.google.protobuf.nano.Extensions; +import com.google.protobuf.nano.Extensions.AnotherMessage; import com.google.protobuf.nano.InternalNano; import com.google.protobuf.nano.MessageNano; import com.google.protobuf.nano.NanoOuterClass; @@ -37,10 +40,12 @@ import com.google.protobuf.nano.NanoOuterClass.TestAllTypesNano; import com.google.protobuf.nano.RecursiveMessageNano; import com.google.protobuf.nano.SimpleMessageNano; import com.google.protobuf.nano.UnittestImportNano; -import com.google.protobuf.nano.CodedInputByteBufferNano; import junit.framework.TestCase; +import java.util.ArrayList; +import java.util.List; + /** * Test nano runtime. * @@ -2155,4 +2160,93 @@ public class NanoTest extends TestCase { assertTrue(protoPrint.contains(" default_int32: 41")); assertTrue(protoPrint.contains(" default_string: \"hello\"")); } + + public void testExtensions() throws Exception { + Extensions.ExtendableMessage message = new Extensions.ExtendableMessage(); + message.field = 5; + message.setExtension(Extensions.someString, "Hello World!"); + message.setExtension(Extensions.someBool, true); + message.setExtension(Extensions.someInt, 42); + message.setExtension(Extensions.someLong, 124234234234L); + message.setExtension(Extensions.someFloat, 42.0f); + message.setExtension(Extensions.someDouble, 422222.0); + message.setExtension(Extensions.someEnum, Extensions.FIRST_VALUE); + AnotherMessage another = new AnotherMessage(); + another.string = "Foo"; + another.value = true; + message.setExtension(Extensions.someMessage, another); + + message.setExtension(Extensions.someRepeatedString, list("a", "bee", "seeya")); + message.setExtension(Extensions.someRepeatedBool, list(true, false, true)); + message.setExtension(Extensions.someRepeatedInt, list(4, 8, 15, 16, 23, 42)); + message.setExtension(Extensions.someRepeatedLong, list(4L, 8L, 15L, 16L, 23L, 42L)); + message.setExtension(Extensions.someRepeatedFloat, list(1.0f, 3.0f)); + message.setExtension(Extensions.someRepeatedDouble, list(55.133, 3.14159)); + message.setExtension(Extensions.someRepeatedEnum, list(Extensions.FIRST_VALUE, + Extensions.SECOND_VALUE)); + AnotherMessage second = new AnotherMessage(); + second.string = "Whee"; + second.value = false; + message.setExtension(Extensions.someRepeatedMessage, list(another, second)); + + byte[] data = MessageNano.toByteArray(message); + + Extensions.ExtendableMessage deserialized = Extensions.ExtendableMessage.parseFrom(data); + assertEquals(5, deserialized.field); + assertEquals("Hello World!", deserialized.getExtension(Extensions.someString)); + assertEquals(Boolean.TRUE, deserialized.getExtension(Extensions.someBool)); + assertEquals(Integer.valueOf(42), deserialized.getExtension(Extensions.someInt)); + assertEquals(Long.valueOf(124234234234L), deserialized.getExtension(Extensions.someLong)); + assertEquals(Float.valueOf(42.0f), deserialized.getExtension(Extensions.someFloat)); + assertEquals(Double.valueOf(422222.0), deserialized.getExtension(Extensions.someDouble)); + assertEquals(Integer.valueOf(Extensions.FIRST_VALUE), + deserialized.getExtension(Extensions.someEnum)); + assertEquals(another.string, deserialized.getExtension(Extensions.someMessage).string); + assertEquals(another.value, deserialized.getExtension(Extensions.someMessage).value); + assertEquals(list("a", "bee", "seeya"), deserialized.getExtension(Extensions.someRepeatedString)); + assertEquals(list(true, false, true), deserialized.getExtension(Extensions.someRepeatedBool)); + assertEquals(list(4, 8, 15, 16, 23, 42), deserialized.getExtension(Extensions.someRepeatedInt)); + assertEquals(list(4L, 8L, 15L, 16L, 23L, 42L), deserialized.getExtension(Extensions.someRepeatedLong)); + assertEquals(list(1.0f, 3.0f), deserialized.getExtension(Extensions.someRepeatedFloat)); + assertEquals(list(55.133, 3.14159), deserialized.getExtension(Extensions.someRepeatedDouble)); + assertEquals(list(Extensions.FIRST_VALUE, + Extensions.SECOND_VALUE), deserialized.getExtension(Extensions.someRepeatedEnum)); + assertEquals("Foo", deserialized.getExtension(Extensions.someRepeatedMessage).get(0).string); + assertEquals(true, deserialized.getExtension(Extensions.someRepeatedMessage).get(0).value); + assertEquals("Whee", deserialized.getExtension(Extensions.someRepeatedMessage).get(1).string); + assertEquals(false, deserialized.getExtension(Extensions.someRepeatedMessage).get(1).value); + } + + public void testUnknownFields() throws Exception { + // Check that we roundtrip (serialize and deserialize) unrecognized fields. + AnotherMessage message = new AnotherMessage(); + message.string = "Hello World"; + message.value = false; + + byte[] bytes = MessageNano.toByteArray(message); + int extraFieldSize = CodedOutputStream.computeStringSize(1001, "This is an unknown field"); + byte[] newBytes = new byte[bytes.length + extraFieldSize]; + System.arraycopy(bytes, 0, newBytes, 0, bytes.length); + CodedOutputStream.newInstance(newBytes, bytes.length, extraFieldSize).writeString(1001, + "This is an unknown field"); + + // Deserialize with an unknown field. + AnotherMessage deserialized = AnotherMessage.parseFrom(newBytes); + byte[] serialized = MessageNano.toByteArray(deserialized); + + assertEquals(newBytes.length, serialized.length); + + // Clear, and make sure it clears everything. + deserialized.clear(); + assertEquals(0, MessageNano.toByteArray(deserialized).length); + } + + private <T> List<T> list(T first, T... remaining) { + List<T> list = new ArrayList<T>(); + list.add(first); + for (T item : remaining) { + list.add(item); + } + return list; + } } |