From 79f19eb9f1d97aff7c6ab14eda49ee8888e51b33 Mon Sep 17 00:00:00 2001 From: Juan Silveira Date: Tue, 17 Jun 2014 15:01:22 +0100 Subject: [PATCH] Keep pointers to extension values. The current implementation of getExtension deserialises the field from bytes and returns a new object every time. This means that changes to those objects are reflected when the messages is serialised unless setExtension is called. It also means that every call to getExtension and setExtension is expensive. This change introduces a FieldData class that contains everything that's known about the field at the time. This can be all the tag/byte[] pairs associated with a given field or an Extension and a value object. This is so that two messages with a repeated extension can be compared even if the extension has been deserialised in one of them but not the other. This change also adds FieldArray class based on SparseArray from the Android compatibility library. This is used in ExtendableMessageNano to make lookup of FieldDatas by their field number faster. Implications: * calling getExtension multiple times deserialises the field only once and returns the same object. * calling setExtension doesn't cause the object to be serialised immediately, that only happens when the container message is serialised. * getExtension is no longer a read-only thread-safe operation. README.txt has been updated to relfect that. * comparison using equals and hashCode continues to work. Bug: 10863158 Change-Id: I81c7cb0c73cc0611a1f7c1eabf5eed259738e8bc --- java/README.txt | 7 +- .../protobuf/nano/ExtendableMessageNano.java | 71 ++- .../com/google/protobuf/nano/Extension.java | 446 ++++++++++-------- .../com/google/protobuf/nano/FieldArray.java | 273 +++++++++++ .../com/google/protobuf/nano/FieldData.java | 173 +++++++ .../protobuf/nano/UnknownFieldData.java | 63 ++- .../java/com/google/protobuf/NanoTest.java | 137 ++++++ 7 files changed, 917 insertions(+), 253 deletions(-) create mode 100644 java/src/main/java/com/google/protobuf/nano/FieldArray.java create mode 100644 java/src/main/java/com/google/protobuf/nano/FieldData.java diff --git a/java/README.txt b/java/README.txt index c6933138a7..4dfef1422a 100644 --- a/java/README.txt +++ b/java/README.txt @@ -442,9 +442,10 @@ used simultaneously from multiple threads in a read-only manner. In other words, an appropriate synchronization mechanism (such as a ReadWriteLock) must be used to ensure that a message, its ancestors, and descendants are not accessed by any other threads -while the message is being modified. Field reads, getter methods, -toByteArray(...), writeTo(...), getCachedSize(), and -getSerializedSize() are all considered read-only operations. +while the message is being modified. Field reads, getter methods +(but not getExtension(...)), toByteArray(...), writeTo(...), +getCachedSize(), and getSerializedSize() are all considered read-only +operations. IMPORTANT: If you have fields with defaults and opt out of accessors diff --git a/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java b/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java index 63c8afcbf5..a0c273110c 100644 --- a/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java +++ b/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java @@ -31,8 +31,6 @@ package com.google.protobuf.nano; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; /** * Base class of those Protocol Buffer messages that need to store unknown fields, @@ -44,27 +42,28 @@ public abstract class ExtendableMessageNano> * A container for fields unknown to the message, including extensions. Extension fields can * can be accessed through the {@link #getExtension} and {@link #setExtension} methods. */ - protected List unknownFieldData; + protected FieldArray unknownFieldData; @Override protected int computeSerializedSize() { int size = 0; - int unknownFieldCount = unknownFieldData == null ? 0 : unknownFieldData.size(); - for (int i = 0; i < unknownFieldCount; i++) { - UnknownFieldData unknownField = unknownFieldData.get(i); - size += CodedOutputByteBufferNano.computeRawVarint32Size(unknownField.tag); - size += unknownField.bytes.length; + if (unknownFieldData != null) { + for (int i = 0; i < unknownFieldData.size(); i++) { + FieldData field = unknownFieldData.dataAt(i); + size += field.computeSerializedSize(); + } } return size; } @Override public void writeTo(CodedOutputByteBufferNano output) throws IOException { - int unknownFieldCount = unknownFieldData == null ? 0 : unknownFieldData.size(); - for (int i = 0; i < unknownFieldCount; i++) { - UnknownFieldData unknownField = unknownFieldData.get(i); - output.writeRawVarint32(unknownField.tag); - output.writeRawBytes(unknownField.bytes); + if (unknownFieldData == null) { + return; + } + for (int i = 0; i < unknownFieldData.size(); i++) { + FieldData field = unknownFieldData.dataAt(i); + field.writeTo(output); } } @@ -72,14 +71,38 @@ public abstract class ExtendableMessageNano> * Gets the value stored in the specified extension of this message. */ public final T getExtension(Extension extension) { - return extension.getValueFrom(unknownFieldData); + if (unknownFieldData == null) { + return null; + } + FieldData field = unknownFieldData.get(WireFormatNano.getTagFieldNumber(extension.tag)); + return field == null ? null : field.getValue(extension); } /** * Sets the value of the specified extension of this message. */ public final M setExtension(Extension extension, T value) { - unknownFieldData = extension.setValueTo(value, unknownFieldData); + int fieldNumber = WireFormatNano.getTagFieldNumber(extension.tag); + if (value == null) { + if (unknownFieldData != null) { + unknownFieldData.remove(fieldNumber); + if (unknownFieldData.isEmpty()) { + unknownFieldData = null; + } + } + } else { + FieldData field = null; + if (unknownFieldData == null) { + unknownFieldData = new FieldArray(); + } else { + field = unknownFieldData.get(fieldNumber); + } + if (field == null) { + unknownFieldData.put(fieldNumber, new FieldData(extension, value)); + } else { + field.setValue(extension, value); + } + } @SuppressWarnings("unchecked") // Generated code should guarantee type safety M typedThis = (M) this; @@ -106,12 +129,22 @@ public abstract class ExtendableMessageNano> if (!input.skipField(tag)) { return false; // This wasn't an unknown field, it's an end-group tag. } - if (unknownFieldData == null) { - unknownFieldData = new ArrayList(); - } + int fieldNumber = WireFormatNano.getTagFieldNumber(tag); int endPos = input.getPosition(); byte[] bytes = input.getData(startPos, endPos - startPos); - unknownFieldData.add(new UnknownFieldData(tag, bytes)); + UnknownFieldData unknownField = new UnknownFieldData(tag, bytes); + + FieldData field = null; + if (unknownFieldData == null) { + unknownFieldData = new FieldArray(); + } else { + field = unknownFieldData.get(fieldNumber); + } + if (field == null) { + field = new FieldData(); + unknownFieldData.put(fieldNumber, field); + } + field.addUnknownField(unknownField); return true; } } diff --git a/java/src/main/java/com/google/protobuf/nano/Extension.java b/java/src/main/java/com/google/protobuf/nano/Extension.java index dfe4f87ddf..962f66ef6e 100644 --- a/java/src/main/java/com/google/protobuf/nano/Extension.java +++ b/java/src/main/java/com/google/protobuf/nano/Extension.java @@ -152,56 +152,50 @@ public class Extension, T> { this.repeated = repeated; } - protected boolean isMatch(int unknownDataTag) { - // This implementation is for message/group extensions. - return unknownDataTag == tag; - } - /** * Returns the value of this extension stored in the given list of unknown fields, or * {@code null} if no unknown fields matches this extension. + * + * @param unknownFields a list of {@link UnknownFieldData}. All of the elements must have a tag + * that matches this Extension's tag. + * */ final T getValueFrom(List unknownFields) { if (unknownFields == null) { return null; } + return repeated ? getRepeatedValueFrom(unknownFields) : getSingularValueFrom(unknownFields); + } - if (repeated) { - // For repeated extensions, read all matching unknown fields in their original order. - List resultList = new ArrayList(); - for (int i = 0; i < unknownFields.size(); i++) { - UnknownFieldData data = unknownFields.get(i); - if (isMatch(data.tag) && data.bytes.length != 0) { - readDataInto(data, resultList); - } - } - - int resultSize = resultList.size(); - if (resultSize == 0) { - return null; + private T getRepeatedValueFrom(List unknownFields) { + // For repeated extensions, read all matching unknown fields in their original order. + List resultList = new ArrayList(); + for (int i = 0; i < unknownFields.size(); i++) { + UnknownFieldData data = unknownFields.get(i); + if (data.bytes.length != 0) { + readDataInto(data, resultList); } + } + int resultSize = resultList.size(); + if (resultSize == 0) { + return null; + } else { T result = clazz.cast(Array.newInstance(clazz.getComponentType(), resultSize)); for (int i = 0; i < resultSize; i++) { Array.set(result, i, resultList.get(i)); } return result; - } else { - // For singular extensions, get the last piece of data stored under this extension. - UnknownFieldData lastData = null; - for (int i = unknownFields.size() - 1; lastData == null && i >= 0; i--) { - UnknownFieldData data = unknownFields.get(i); - if (isMatch(data.tag) && data.bytes.length != 0) { - lastData = data; - } - } - - if (lastData == null) { - return null; - } + } + } - return clazz.cast(readData(CodedInputByteBufferNano.newInstance(lastData.bytes))); + private T getSingularValueFrom(List unknownFields) { + // For singular extensions, get the last piece of data stored under this extension. + if (unknownFields.isEmpty()) { + return null; } + UnknownFieldData lastData = unknownFields.get(unknownFields.size() - 1); + return clazz.cast(readData(CodedInputByteBufferNano.newInstance(lastData.bytes))); } protected Object readData(CodedInputByteBufferNano input) { @@ -236,61 +230,29 @@ public class Extension, T> { resultList.add(readData(CodedInputByteBufferNano.newInstance(data.bytes))); } - /** - * Sets the value of this extension to the given list of unknown fields. This removes any - * previously stored data matching this extension. - * - * @param value The value of this extension, or {@code null} to clear this extension from the - * unknown fields. - * @return The same {@code unknownFields} list, or a new list storing the extension value if - * the argument was null. - */ - final List setValueTo(T value, List unknownFields) { - if (unknownFields != null) { - // Delete all data matching this extension - for (int i = unknownFields.size() - 1; i >= 0; i--) { - if (isMatch(unknownFields.get(i).tag)) { - unknownFields.remove(i); - } - } - } - - if (value != null) { - if (unknownFields == null) { - unknownFields = new ArrayList(); - } - if (repeated) { - writeDataInto(value, unknownFields); - } else { - unknownFields.add(writeData(value)); - } + void writeTo(Object value, CodedOutputByteBufferNano output) throws IOException { + if (repeated) { + writeRepeatedData(value, output); + } else { + writeSingularData(value, output); } - - // After deletion or no-op addition (due to 'value' being an array of empty or - // null-only elements), unknownFields may be empty. Discard the ArrayList if so. - return (unknownFields == null || unknownFields.isEmpty()) ? null : unknownFields; } - protected UnknownFieldData writeData(Object value) { + protected void writeSingularData(Object value, CodedOutputByteBufferNano out) { // This implementation is for message/group extensions. - byte[] data; try { + out.writeRawVarint32(tag); switch (type) { case TYPE_GROUP: MessageNano groupValue = (MessageNano) value; int fieldNumber = WireFormatNano.getTagFieldNumber(tag); - data = new byte[CodedOutputByteBufferNano.computeGroupSizeNoTag(groupValue) - + CodedOutputByteBufferNano.computeTagSize(fieldNumber)]; - CodedOutputByteBufferNano out = CodedOutputByteBufferNano.newInstance(data); out.writeGroupNoTag(groupValue); // The endgroup tag must be included in the data payload. out.writeTag(fieldNumber, WireFormatNano.WIRETYPE_END_GROUP); break; case TYPE_MESSAGE: MessageNano messageValue = (MessageNano) value; - data = new byte[ - CodedOutputByteBufferNano.computeMessageSizeNoTag(messageValue)]; - CodedOutputByteBufferNano.newInstance(data).writeMessageNoTag(messageValue); + out.writeMessageNoTag(messageValue); break; default: throw new IllegalArgumentException("Unknown type " + type); @@ -299,18 +261,53 @@ public class Extension, T> { // Should not happen throw new IllegalStateException(e); } - return new UnknownFieldData(tag, data); } - protected void writeDataInto(T array, List unknownFields) { + protected void writeRepeatedData(Object array, CodedOutputByteBufferNano output) { + // This implementation is for non-packed extensions. + int arrayLength = Array.getLength(array); + for (int i = 0; i < arrayLength; i++) { + Object element = Array.get(array, i); + if (element != null) { + writeSingularData(element, output); + } + } + } + + int computeSerializedSize(Object value) { + if (repeated) { + return computeRepeatedSerializedSize(value); + } else { + return computeSingularSerializedSize(value); + } + } + + protected int computeRepeatedSerializedSize(Object array) { // This implementation is for non-packed extensions. + int size = 0; int arrayLength = Array.getLength(array); for (int i = 0; i < arrayLength; i++) { Object element = Array.get(array, i); if (element != null) { - unknownFields.add(writeData(element)); + size += computeSingularSerializedSize(Array.get(array, i)); } } + return size; + } + + protected int computeSingularSerializedSize(Object value) { + // This implementation is for message/group extensions. + int fieldNumber = WireFormatNano.getTagFieldNumber(tag); + switch (type) { + case TYPE_GROUP: + MessageNano groupValue = (MessageNano) value; + return CodedOutputByteBufferNano.computeGroupSize(fieldNumber, groupValue); + case TYPE_MESSAGE: + MessageNano messageValue = (MessageNano) value; + return CodedOutputByteBufferNano.computeMessageSize(fieldNumber, messageValue); + default: + throw new IllegalArgumentException("Unknown type " + type); + } } /** @@ -338,15 +335,6 @@ public class Extension, T> { this.packedTag = packedTag; } - @Override - protected boolean isMatch(int unknownDataTag) { - if (repeated) { - return unknownDataTag == nonPackedTag || unknownDataTag == packedTag; - } else { - return unknownDataTag == tag; - } - } - @Override protected Object readData(CodedInputByteBufferNano input) { try { @@ -398,7 +386,8 @@ public class Extension, T> { if (data.tag == nonPackedTag) { resultList.add(readData(CodedInputByteBufferNano.newInstance(data.bytes))); } else { - CodedInputByteBufferNano buffer = CodedInputByteBufferNano.newInstance(data.bytes); + CodedInputByteBufferNano buffer = + CodedInputByteBufferNano.newInstance(data.bytes); try { buffer.pushLimit(buffer.readRawVarint32()); // length limit } catch (IOException e) { @@ -411,105 +400,73 @@ public class Extension, T> { } @Override - protected final UnknownFieldData writeData(Object value) { - byte[] data; + protected final void writeSingularData(Object value, CodedOutputByteBufferNano output) { try { + output.writeRawVarint32(tag); switch (type) { case TYPE_DOUBLE: Double doubleValue = (Double) value; - data = new byte[ - CodedOutputByteBufferNano.computeDoubleSizeNoTag(doubleValue)]; - CodedOutputByteBufferNano.newInstance(data).writeDoubleNoTag(doubleValue); + output.writeDoubleNoTag(doubleValue); break; case TYPE_FLOAT: Float floatValue = (Float) value; - data = new byte[ - CodedOutputByteBufferNano.computeFloatSizeNoTag(floatValue)]; - CodedOutputByteBufferNano.newInstance(data).writeFloatNoTag(floatValue); + output.writeFloatNoTag(floatValue); break; case TYPE_INT64: Long int64Value = (Long) value; - data = new byte[ - CodedOutputByteBufferNano.computeInt64SizeNoTag(int64Value)]; - CodedOutputByteBufferNano.newInstance(data).writeInt64NoTag(int64Value); + output.writeInt64NoTag(int64Value); break; case TYPE_UINT64: Long uint64Value = (Long) value; - data = new byte[ - CodedOutputByteBufferNano.computeUInt64SizeNoTag(uint64Value)]; - CodedOutputByteBufferNano.newInstance(data).writeUInt64NoTag(uint64Value); + output.writeUInt64NoTag(uint64Value); break; case TYPE_INT32: Integer int32Value = (Integer) value; - data = new byte[ - CodedOutputByteBufferNano.computeInt32SizeNoTag(int32Value)]; - CodedOutputByteBufferNano.newInstance(data).writeInt32NoTag(int32Value); + output.writeInt32NoTag(int32Value); break; case TYPE_FIXED64: Long fixed64Value = (Long) value; - data = new byte[ - CodedOutputByteBufferNano.computeFixed64SizeNoTag(fixed64Value)]; - CodedOutputByteBufferNano.newInstance(data).writeFixed64NoTag(fixed64Value); + output.writeFixed64NoTag(fixed64Value); break; case TYPE_FIXED32: Integer fixed32Value = (Integer) value; - data = new byte[ - CodedOutputByteBufferNano.computeFixed32SizeNoTag(fixed32Value)]; - CodedOutputByteBufferNano.newInstance(data).writeFixed32NoTag(fixed32Value); + output.writeFixed32NoTag(fixed32Value); break; case TYPE_BOOL: Boolean boolValue = (Boolean) value; - data = new byte[CodedOutputByteBufferNano.computeBoolSizeNoTag(boolValue)]; - CodedOutputByteBufferNano.newInstance(data).writeBoolNoTag(boolValue); + output.writeBoolNoTag(boolValue); break; case TYPE_STRING: String stringValue = (String) value; - data = new byte[ - CodedOutputByteBufferNano.computeStringSizeNoTag(stringValue)]; - CodedOutputByteBufferNano.newInstance(data).writeStringNoTag(stringValue); + output.writeStringNoTag(stringValue); break; case TYPE_BYTES: byte[] bytesValue = (byte[]) value; - data = new byte[ - CodedOutputByteBufferNano.computeBytesSizeNoTag(bytesValue)]; - CodedOutputByteBufferNano.newInstance(data).writeBytesNoTag(bytesValue); + output.writeBytesNoTag(bytesValue); break; case TYPE_UINT32: Integer uint32Value = (Integer) value; - data = new byte[ - CodedOutputByteBufferNano.computeUInt32SizeNoTag(uint32Value)]; - CodedOutputByteBufferNano.newInstance(data).writeUInt32NoTag(uint32Value); + output.writeUInt32NoTag(uint32Value); break; case TYPE_ENUM: Integer enumValue = (Integer) value; - data = new byte[CodedOutputByteBufferNano.computeEnumSizeNoTag(enumValue)]; - CodedOutputByteBufferNano.newInstance(data).writeEnumNoTag(enumValue); + output.writeEnumNoTag(enumValue); break; case TYPE_SFIXED32: Integer sfixed32Value = (Integer) value; - data = new byte[ - CodedOutputByteBufferNano.computeSFixed32SizeNoTag(sfixed32Value)]; - CodedOutputByteBufferNano.newInstance(data) - .writeSFixed32NoTag(sfixed32Value); + output.writeSFixed32NoTag(sfixed32Value); break; case TYPE_SFIXED64: Long sfixed64Value = (Long) value; - data = new byte[ - CodedOutputByteBufferNano.computeSFixed64SizeNoTag(sfixed64Value)]; - CodedOutputByteBufferNano.newInstance(data) - .writeSFixed64NoTag(sfixed64Value); + output.writeSFixed64NoTag(sfixed64Value); break; case TYPE_SINT32: Integer sint32Value = (Integer) value; - data = new byte[ - CodedOutputByteBufferNano.computeSInt32SizeNoTag(sint32Value)]; - CodedOutputByteBufferNano.newInstance(data).writeSInt32NoTag(sint32Value); + output.writeSInt32NoTag(sint32Value); break; case TYPE_SINT64: Long sint64Value = (Long) value; - data = new byte[ - CodedOutputByteBufferNano.computeSInt64SizeNoTag(sint64Value)]; - CodedOutputByteBufferNano.newInstance(data).writeSInt64NoTag(sint64Value); + output.writeSInt64NoTag(sint64Value); break; default: throw new IllegalArgumentException("Unknown type " + type); @@ -518,86 +475,21 @@ public class Extension, T> { // Should not happen throw new IllegalStateException(e); } - return new UnknownFieldData(tag, data); } @Override - protected void writeDataInto(T array, List unknownFields) { + protected void writeRepeatedData(Object array, CodedOutputByteBufferNano output) { if (tag == nonPackedTag) { // Use base implementation for non-packed data - super.writeDataInto(array, unknownFields); + super.writeRepeatedData(array, output); } else if (tag == packedTag) { // Packed. Note that the array element type is guaranteed to be primitive, so there - // won't be any null elements, so no null check in this block. First get data size. + // won't be any null elements, so no null check in this block. int arrayLength = Array.getLength(array); - int dataSize = 0; - switch (type) { - case TYPE_BOOL: - // Bools are stored as int32 but just as 0 or 1, so 1 byte each. - dataSize = arrayLength; - break; - case TYPE_FIXED32: - case TYPE_SFIXED32: - case TYPE_FLOAT: - dataSize = arrayLength * CodedOutputByteBufferNano.LITTLE_ENDIAN_32_SIZE; - break; - case TYPE_FIXED64: - case TYPE_SFIXED64: - case TYPE_DOUBLE: - dataSize = arrayLength * CodedOutputByteBufferNano.LITTLE_ENDIAN_64_SIZE; - break; - case TYPE_INT32: - for (int i = 0; i < arrayLength; i++) { - dataSize += CodedOutputByteBufferNano.computeInt32SizeNoTag( - Array.getInt(array, i)); - } - break; - case TYPE_SINT32: - for (int i = 0; i < arrayLength; i++) { - dataSize += CodedOutputByteBufferNano.computeSInt32SizeNoTag( - Array.getInt(array, i)); - } - break; - case TYPE_UINT32: - for (int i = 0; i < arrayLength; i++) { - dataSize += CodedOutputByteBufferNano.computeUInt32SizeNoTag( - Array.getInt(array, i)); - } - break; - case TYPE_INT64: - for (int i = 0; i < arrayLength; i++) { - dataSize += CodedOutputByteBufferNano.computeInt64SizeNoTag( - Array.getLong(array, i)); - } - break; - case TYPE_SINT64: - for (int i = 0; i < arrayLength; i++) { - dataSize += CodedOutputByteBufferNano.computeSInt64SizeNoTag( - Array.getLong(array, i)); - } - break; - case TYPE_UINT64: - for (int i = 0; i < arrayLength; i++) { - dataSize += CodedOutputByteBufferNano.computeUInt64SizeNoTag( - Array.getLong(array, i)); - } - break; - case TYPE_ENUM: - for (int i = 0; i < arrayLength; i++) { - dataSize += CodedOutputByteBufferNano.computeEnumSizeNoTag( - Array.getInt(array, i)); - } - break; - default: - throw new IllegalArgumentException("Unexpected non-packable type " + type); - } + int dataSize = computePackedDataSize(array); - // Then construct payload. - int payloadSize = - dataSize + CodedOutputByteBufferNano.computeRawVarint32Size(dataSize); - byte[] data = new byte[payloadSize]; - CodedOutputByteBufferNano output = CodedOutputByteBufferNano.newInstance(data); try { + output.writeRawVarint32(tag); output.writeRawVarint32(dataSize); switch (type) { case TYPE_BOOL: @@ -677,12 +569,154 @@ public class Extension, T> { // Should not happen. throw new IllegalStateException(e); } - unknownFields.add(new UnknownFieldData(tag, data)); } else { throw new IllegalArgumentException("Unexpected repeated extension tag " + tag + ", unequal to both non-packed variant " + nonPackedTag + " and packed variant " + packedTag); } } + + private int computePackedDataSize(Object array) { + int dataSize = 0; + int arrayLength = Array.getLength(array); + switch (type) { + case TYPE_BOOL: + // Bools are stored as int32 but just as 0 or 1, so 1 byte each. + dataSize = arrayLength; + break; + case TYPE_FIXED32: + case TYPE_SFIXED32: + case TYPE_FLOAT: + dataSize = arrayLength * CodedOutputByteBufferNano.LITTLE_ENDIAN_32_SIZE; + break; + case TYPE_FIXED64: + case TYPE_SFIXED64: + case TYPE_DOUBLE: + dataSize = arrayLength * CodedOutputByteBufferNano.LITTLE_ENDIAN_64_SIZE; + break; + case TYPE_INT32: + for (int i = 0; i < arrayLength; i++) { + dataSize += CodedOutputByteBufferNano.computeInt32SizeNoTag( + Array.getInt(array, i)); + } + break; + case TYPE_SINT32: + for (int i = 0; i < arrayLength; i++) { + dataSize += CodedOutputByteBufferNano.computeSInt32SizeNoTag( + Array.getInt(array, i)); + } + break; + case TYPE_UINT32: + for (int i = 0; i < arrayLength; i++) { + dataSize += CodedOutputByteBufferNano.computeUInt32SizeNoTag( + Array.getInt(array, i)); + } + break; + case TYPE_INT64: + for (int i = 0; i < arrayLength; i++) { + dataSize += CodedOutputByteBufferNano.computeInt64SizeNoTag( + Array.getLong(array, i)); + } + break; + case TYPE_SINT64: + for (int i = 0; i < arrayLength; i++) { + dataSize += CodedOutputByteBufferNano.computeSInt64SizeNoTag( + Array.getLong(array, i)); + } + break; + case TYPE_UINT64: + for (int i = 0; i < arrayLength; i++) { + dataSize += CodedOutputByteBufferNano.computeUInt64SizeNoTag( + Array.getLong(array, i)); + } + break; + case TYPE_ENUM: + for (int i = 0; i < arrayLength; i++) { + dataSize += CodedOutputByteBufferNano.computeEnumSizeNoTag( + Array.getInt(array, i)); + } + break; + default: + throw new IllegalArgumentException("Unexpected non-packable type " + type); + } + return dataSize; + } + + @Override + protected int computeRepeatedSerializedSize(Object array) { + if (tag == nonPackedTag) { + // Use base implementation for non-packed data + return super.computeRepeatedSerializedSize(array); + } else if (tag == packedTag) { + // Packed. + int dataSize = computePackedDataSize(array); + int payloadSize = + dataSize + CodedOutputByteBufferNano.computeRawVarint32Size(dataSize); + return payloadSize + CodedOutputByteBufferNano.computeRawVarint32Size(tag); + } else { + throw new IllegalArgumentException("Unexpected repeated extension tag " + tag + + ", unequal to both non-packed variant " + nonPackedTag + + " and packed variant " + packedTag); + } + } + + @Override + protected final int computeSingularSerializedSize(Object value) { + int fieldNumber = WireFormatNano.getTagFieldNumber(tag); + switch (type) { + case TYPE_DOUBLE: + Double doubleValue = (Double) value; + return CodedOutputByteBufferNano.computeDoubleSize(fieldNumber, doubleValue); + case TYPE_FLOAT: + Float floatValue = (Float) value; + return CodedOutputByteBufferNano.computeFloatSize(fieldNumber, floatValue); + case TYPE_INT64: + Long int64Value = (Long) value; + return CodedOutputByteBufferNano.computeInt64Size(fieldNumber, int64Value); + case TYPE_UINT64: + Long uint64Value = (Long) value; + return CodedOutputByteBufferNano.computeUInt64Size(fieldNumber, uint64Value); + case TYPE_INT32: + Integer int32Value = (Integer) value; + return CodedOutputByteBufferNano.computeInt32Size(fieldNumber, int32Value); + case TYPE_FIXED64: + Long fixed64Value = (Long) value; + return CodedOutputByteBufferNano.computeFixed64Size(fieldNumber, fixed64Value); + case TYPE_FIXED32: + Integer fixed32Value = (Integer) value; + return CodedOutputByteBufferNano.computeFixed32Size(fieldNumber, fixed32Value); + case TYPE_BOOL: + Boolean boolValue = (Boolean) value; + return CodedOutputByteBufferNano.computeBoolSize(fieldNumber, boolValue); + case TYPE_STRING: + String stringValue = (String) value; + return CodedOutputByteBufferNano.computeStringSize(fieldNumber, stringValue); + case TYPE_BYTES: + byte[] bytesValue = (byte[]) value; + return CodedOutputByteBufferNano.computeBytesSize(fieldNumber, bytesValue); + case TYPE_UINT32: + Integer uint32Value = (Integer) value; + return CodedOutputByteBufferNano.computeUInt32Size(fieldNumber, uint32Value); + case TYPE_ENUM: + Integer enumValue = (Integer) value; + return CodedOutputByteBufferNano.computeEnumSize(fieldNumber, enumValue); + case TYPE_SFIXED32: + Integer sfixed32Value = (Integer) value; + return CodedOutputByteBufferNano.computeSFixed32Size(fieldNumber, + sfixed32Value); + case TYPE_SFIXED64: + Long sfixed64Value = (Long) value; + return CodedOutputByteBufferNano.computeSFixed64Size(fieldNumber, + sfixed64Value); + case TYPE_SINT32: + Integer sint32Value = (Integer) value; + return CodedOutputByteBufferNano.computeSInt32Size(fieldNumber, sint32Value); + case TYPE_SINT64: + Long sint64Value = (Long) value; + return CodedOutputByteBufferNano.computeSInt64Size(fieldNumber, sint64Value); + default: + throw new IllegalArgumentException("Unknown type " + type); + } + } } } diff --git a/java/src/main/java/com/google/protobuf/nano/FieldArray.java b/java/src/main/java/com/google/protobuf/nano/FieldArray.java new file mode 100644 index 0000000000..ab923a4de7 --- /dev/null +++ b/java/src/main/java/com/google/protobuf/nano/FieldArray.java @@ -0,0 +1,273 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2014 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; + + +/** + * A custom version of {@link android.util.SparseArray} with the minimal API + * for storing {@link FieldData} objects. + * + * Based on {@link android.support.v4.util.SpareArrayCompat}. + */ +class FieldArray { + private static final FieldData DELETED = new FieldData(); + private boolean mGarbage = false; + + private int[] mFieldNumbers; + private FieldData[] mData; + private int mSize; + + /** + * Creates a new FieldArray containing no fields. + */ + public FieldArray() { + this(10); + } + + /** + * Creates a new FieldArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. + */ + public FieldArray(int initialCapacity) { + initialCapacity = idealIntArraySize(initialCapacity); + mFieldNumbers = new int[initialCapacity]; + mData = new FieldData[initialCapacity]; + mSize = 0; + } + + /** + * Gets the FieldData mapped from the specified fieldNumber, or null + * if no such mapping has been made. + */ + public FieldData get(int fieldNumber) { + int i = binarySearch(fieldNumber); + + if (i < 0 || mData[i] == DELETED) { + return null; + } else { + return mData[i]; + } + } + + /** + * Removes the data from the specified fieldNumber, if there was any. + */ + public void remove(int fieldNumber) { + int i = binarySearch(fieldNumber); + + if (i >= 0 && mData[i] != DELETED) { + mData[i] = DELETED; + mGarbage = true; + } + } + + private void gc() { + int n = mSize; + int o = 0; + int[] keys = mFieldNumbers; + FieldData[] values = mData; + + for (int i = 0; i < n; i++) { + FieldData val = values[i]; + + if (val != DELETED) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + values[i] = null; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + } + + /** + * Adds a mapping from the specified fieldNumber to the specified data, + * replacing the previous mapping if there was one. + */ + public void put(int fieldNumber, FieldData data) { + int i = binarySearch(fieldNumber); + + if (i >= 0) { + mData[i] = data; + } else { + i = ~i; + + if (i < mSize && mData[i] == DELETED) { + mFieldNumbers[i] = fieldNumber; + mData[i] = data; + return; + } + + if (mGarbage && mSize >= mFieldNumbers.length) { + gc(); + + // Search again because indices may have changed. + i = ~ binarySearch(fieldNumber); + } + + if (mSize >= mFieldNumbers.length) { + int n = idealIntArraySize(mSize + 1); + + int[] nkeys = new int[n]; + FieldData[] nvalues = new FieldData[n]; + + System.arraycopy(mFieldNumbers, 0, nkeys, 0, mFieldNumbers.length); + System.arraycopy(mData, 0, nvalues, 0, mData.length); + + mFieldNumbers = nkeys; + mData = nvalues; + } + + if (mSize - i != 0) { + System.arraycopy(mFieldNumbers, i, mFieldNumbers, i + 1, mSize - i); + System.arraycopy(mData, i, mData, i + 1, mSize - i); + } + + mFieldNumbers[i] = fieldNumber; + mData[i] = data; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this FieldArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + public boolean isEmpty() { + return size() == 0; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * FieldArray stores. + */ + public FieldData dataAt(int index) { + if (mGarbage) { + gc(); + } + + return mData[index]; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof FieldArray)) { + return false; + } + + FieldArray other = (FieldArray) o; + if (size() != other.size()) { // size() will call gc() if necessary. + return false; + } + return arrayEquals(mFieldNumbers, other.mFieldNumbers, mSize) && + arrayEquals(mData, other.mData, mSize); + } + + @Override + public int hashCode() { + if (mGarbage) { + gc(); + } + int result = 17; + for (int i = 0; i < mSize; i++) { + result = 31 * result + mFieldNumbers[i]; + result = 31 * result + mData[i].hashCode(); + } + return result; + } + + private int idealIntArraySize(int need) { + return idealByteArraySize(need * 4) / 4; + } + + private int idealByteArraySize(int need) { + for (int i = 4; i < 32; i++) + if (need <= (1 << i) - 12) + return (1 << i) - 12; + + return need; + } + + private int binarySearch(int value) { + int lo = 0; + int hi = mSize - 1; + + while (lo <= hi) { + int mid = (lo + hi) >>> 1; + int midVal = mFieldNumbers[mid]; + + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } + + private boolean arrayEquals(int[] a, int[] b, int size) { + for (int i = 0; i < size; i++) { + if (a[i] != b[i]) { + return false; + } + } + return true; + } + + private boolean arrayEquals(FieldData[] a, FieldData[] b, int size) { + for (int i = 0; i < size; i++) { + if (!a[i].equals(b[i])) { + return false; + } + } + return true; + } +} diff --git a/java/src/main/java/com/google/protobuf/nano/FieldData.java b/java/src/main/java/com/google/protobuf/nano/FieldData.java new file mode 100644 index 0000000000..7a5eb4c1f3 --- /dev/null +++ b/java/src/main/java/com/google/protobuf/nano/FieldData.java @@ -0,0 +1,173 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2014 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.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Stores unknown fields. These might be extensions or fields that the generated API doesn't + * know about yet. + */ +class FieldData { + private Extension cachedExtension; + private Object value; + /** The serialised values for this object. Will be cleared if getValue is called */ + private List unknownFieldData; + + FieldData(Extension extension, T newValue) { + cachedExtension = extension; + value = newValue; + } + + FieldData() { + unknownFieldData = new ArrayList(); + } + + void addUnknownField(UnknownFieldData unknownField) { + unknownFieldData.add(unknownField); + } + + T getValue(Extension extension) { + if (value != null){ + if (cachedExtension != extension) { // Extension objects are singletons. + throw new IllegalStateException( + "Tried to getExtension with a differernt Extension."); + } + } else { + cachedExtension = extension; + value = extension.getValueFrom(unknownFieldData); + unknownFieldData = null; + } + return (T) value; + } + + void setValue(Extension extension, T newValue) { + cachedExtension = extension; + value = newValue; + unknownFieldData = null; + } + + int computeSerializedSize() { + int size = 0; + if (value != null) { + size = cachedExtension.computeSerializedSize(value); + } else { + for (UnknownFieldData unknownField : unknownFieldData) { + size += unknownField.computeSerializedSize(); + } + } + return size; + } + + void writeTo(CodedOutputByteBufferNano output) throws IOException { + if (value != null) { + cachedExtension.writeTo(value, output); + } else { + for (UnknownFieldData unknownField : unknownFieldData) { + unknownField.writeTo(output); + } + } + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof FieldData)) { + return false; + } + + FieldData other = (FieldData) o; + if (value != null && other.value != null) { + // If both objects have deserialized values, compare those. + // Since unknown fields are only compared if messages have generated equals methods + // we know this will be a meaningful comparison (not identity) for all values. + if (cachedExtension != other.cachedExtension) { // Extension objects are singletons. + return false; + } + if (!cachedExtension.clazz.isArray()) { + // Can't test (!cachedExtension.repeated) due to 'bytes' -> 'byte[]' + return value.equals(other.value); + } + if (value instanceof byte[]) { + return Arrays.equals((byte[]) value, (byte[]) other.value); + } else if (value instanceof int[]) { + return Arrays.equals((int[]) value, (int[]) other.value); + } else if (value instanceof long[]) { + return Arrays.equals((long[]) value, (long[]) other.value); + } else if (value instanceof float[]) { + return Arrays.equals((float[]) value, (float[]) other.value); + } else if (value instanceof double[]) { + return Arrays.equals((double[]) value, (double[]) other.value); + } else if (value instanceof boolean[]) { + return Arrays.equals((boolean[]) value, (boolean[]) other.value); + } else { + return Arrays.deepEquals((Object[]) value, (Object[]) other.value); + } + } + if (unknownFieldData != null && other.unknownFieldData != null) { + // If both objects have byte arrays compare those directly. + return unknownFieldData.equals(other.unknownFieldData); + } + try { + // As a last resort, serialize and compare the resulting byte arrays. + return Arrays.equals(toByteArray(), other.toByteArray()); + } catch (IOException e) { + // Should not happen. + throw new IllegalStateException(e); + } + } + + @Override + public int hashCode() { + int result = 17; + try { + // The only way to generate a consistent hash is to use the serialized form. + result = 31 * result + Arrays.hashCode(toByteArray()); + } catch (IOException e) { + // Should not happen. + throw new IllegalStateException(e); + } + return result; + } + + private byte[] toByteArray() throws IOException { + byte[] result = new byte[computeSerializedSize()]; + CodedOutputByteBufferNano output = CodedOutputByteBufferNano.newInstance(result); + writeTo(output); + return result; + } + +} diff --git a/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java b/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java index 833ed2a97f..2032e1a6b5 100644 --- a/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java +++ b/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java @@ -30,42 +30,55 @@ package com.google.protobuf.nano; +import java.io.IOException; import java.util.Arrays; /** - * Stores unknown fields. These might be extensions or fields that the generated API doesn't - * know about yet. + * 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 class UnknownFieldData { - final int tag; - final byte[] bytes; + final int tag; + final byte[] bytes; - UnknownFieldData(int tag, byte[] bytes) { - this.tag = tag; - this.bytes = bytes; - } + UnknownFieldData(int tag, byte[] bytes) { + this.tag = tag; + this.bytes = bytes; + } - @Override - public boolean equals(Object o) { - if (o == this) { - return true; + int computeSerializedSize() { + int size = 0; + size += CodedOutputByteBufferNano.computeRawVarint32Size(tag); + size += bytes.length; + return size; } - if (!(o instanceof UnknownFieldData)) { - return false; + + void writeTo(CodedOutputByteBufferNano output) throws IOException { + output.writeRawVarint32(tag); + output.writeRawBytes(bytes); } - UnknownFieldData other = (UnknownFieldData) o; - return tag == other.tag && Arrays.equals(bytes, other.bytes); - } + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof UnknownFieldData)) { + return false; + } - @Override - public int hashCode() { - int result = 17; - result = 31 * result + tag; - result = 31 * result + Arrays.hashCode(bytes); - return result; - } + UnknownFieldData other = (UnknownFieldData) o; + return tag == other.tag && Arrays.equals(bytes, other.bytes); + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + tag; + result = 31 * result + Arrays.hashCode(bytes); + return result; + } } diff --git a/java/src/test/java/com/google/protobuf/NanoTest.java b/java/src/test/java/com/google/protobuf/NanoTest.java index 00e2597edd..93c9dc4251 100644 --- a/java/src/test/java/com/google/protobuf/NanoTest.java +++ b/java/src/test/java/com/google/protobuf/NanoTest.java @@ -41,6 +41,7 @@ import com.google.protobuf.nano.Extensions.MessageWithGroup; import com.google.protobuf.nano.FileScopeEnumMultiple; import com.google.protobuf.nano.FileScopeEnumRefNano; import com.google.protobuf.nano.InternalNano; +import com.google.protobuf.nano.InvalidProtocolBufferNanoException; import com.google.protobuf.nano.MessageNano; import com.google.protobuf.nano.MessageScopeEnumRefNano; import com.google.protobuf.nano.MultipleImportingNonMultipleNano1; @@ -2826,6 +2827,8 @@ public class NanoTest extends TestCase { assertEquals(group2.a, message.getExtension(SingularExtensions.someGroup).a); // Test reading back using RepeatedExtensions: the arrays should be equal. + message = Extensions.ExtendableMessage.parseFrom(data); + assertEquals(5, message.field); assertTrue(Arrays.equals(int32s, message.getExtension(RepeatedExtensions.repeatedInt32))); assertTrue(Arrays.equals(uint32s, message.getExtension(RepeatedExtensions.repeatedUint32))); assertTrue(Arrays.equals(sint32s, message.getExtension(RepeatedExtensions.repeatedSint32))); @@ -2860,6 +2863,8 @@ public class NanoTest extends TestCase { // Test reading back using PackedExtensions: the arrays should be equal, even the fields // are non-packed. + message = Extensions.ExtendableMessage.parseFrom(data); + assertEquals(5, message.field); assertTrue(Arrays.equals(int32s, message.getExtension(PackedExtensions.packedInt32))); assertTrue(Arrays.equals(uint32s, message.getExtension(PackedExtensions.packedUint32))); assertTrue(Arrays.equals(sint32s, message.getExtension(PackedExtensions.packedSint32))); @@ -2924,6 +2929,138 @@ public class NanoTest extends TestCase { assertEquals(0, MessageNano.toByteArray(message).length); } + public void testExtensionsMutation() { + Extensions.ExtendableMessage extendableMessage = new Extensions.ExtendableMessage(); + extendableMessage.setExtension(SingularExtensions.someMessage, + new Extensions.AnotherMessage()); + + extendableMessage.getExtension(SingularExtensions.someMessage).string = "not empty"; + + assertEquals("not empty", + extendableMessage.getExtension(SingularExtensions.someMessage).string); + } + + public void testExtensionsMutation_Equals() throws InvalidProtocolBufferNanoException { + Extensions.ExtendableMessage extendableMessage = new Extensions.ExtendableMessage(); + extendableMessage.field = 5; + int int32 = 42; + int[] uint32s = {3, 4}; + int[] sint32s = {-5, -6}; + long[] int64s = {7, 8}; + long[] uint64s = {9, 10}; + long[] sint64s = {-11, -12}; + int[] fixed32s = {13, 14}; + int[] sfixed32s = {-15, -16}; + long[] fixed64s = {17, 18}; + long[] sfixed64s = {-19, -20}; + boolean[] bools = {true, false}; + float[] floats = {2.1f, 2.2f}; + double[] doubles = {2.3, 2.4}; + int[] enums = {Extensions.SECOND_VALUE, Extensions.FIRST_VALUE}; + String[] strings = {"vijfentwintig", "twenty-six"}; + byte[][] bytess = {{2, 7}, {2, 8}}; + AnotherMessage another1 = new AnotherMessage(); + another1.string = "er shi jiu"; + another1.value = false; + AnotherMessage another2 = new AnotherMessage(); + another2.string = "trente"; + another2.value = true; + AnotherMessage[] messages = {another1, another2}; + RepeatedExtensions.RepeatedGroup group1 = new RepeatedExtensions.RepeatedGroup(); + group1.a = 31; + RepeatedExtensions.RepeatedGroup group2 = new RepeatedExtensions.RepeatedGroup(); + group2.a = 32; + RepeatedExtensions.RepeatedGroup[] groups = {group1, group2}; + extendableMessage.setExtension(SingularExtensions.someInt32, int32); + extendableMessage.setExtension(RepeatedExtensions.repeatedUint32, uint32s); + extendableMessage.setExtension(RepeatedExtensions.repeatedSint32, sint32s); + extendableMessage.setExtension(RepeatedExtensions.repeatedInt64, int64s); + extendableMessage.setExtension(RepeatedExtensions.repeatedUint64, uint64s); + extendableMessage.setExtension(RepeatedExtensions.repeatedSint64, sint64s); + extendableMessage.setExtension(RepeatedExtensions.repeatedFixed32, fixed32s); + extendableMessage.setExtension(RepeatedExtensions.repeatedSfixed32, sfixed32s); + extendableMessage.setExtension(RepeatedExtensions.repeatedFixed64, fixed64s); + extendableMessage.setExtension(RepeatedExtensions.repeatedSfixed64, sfixed64s); + extendableMessage.setExtension(RepeatedExtensions.repeatedBool, bools); + extendableMessage.setExtension(RepeatedExtensions.repeatedFloat, floats); + extendableMessage.setExtension(RepeatedExtensions.repeatedDouble, doubles); + extendableMessage.setExtension(RepeatedExtensions.repeatedEnum, enums); + extendableMessage.setExtension(RepeatedExtensions.repeatedString, strings); + extendableMessage.setExtension(RepeatedExtensions.repeatedBytes, bytess); + extendableMessage.setExtension(RepeatedExtensions.repeatedMessage, messages); + extendableMessage.setExtension(RepeatedExtensions.repeatedGroup, groups); + + byte[] data = MessageNano.toByteArray(extendableMessage); + + extendableMessage = Extensions.ExtendableMessage.parseFrom(data); + Extensions.ExtendableMessage messageCopy = Extensions.ExtendableMessage.parseFrom(data); + + // Without deserialising. + assertEquals(extendableMessage, messageCopy); + assertEquals(extendableMessage.hashCode(), messageCopy.hashCode()); + + // Only one deserialized. + extendableMessage.getExtension(SingularExtensions.someInt32); + extendableMessage.getExtension(RepeatedExtensions.repeatedUint32); + extendableMessage.getExtension(RepeatedExtensions.repeatedSint32); + extendableMessage.getExtension(RepeatedExtensions.repeatedInt64); + extendableMessage.getExtension(RepeatedExtensions.repeatedUint64); + extendableMessage.getExtension(RepeatedExtensions.repeatedSint64); + extendableMessage.getExtension(RepeatedExtensions.repeatedFixed32); + extendableMessage.getExtension(RepeatedExtensions.repeatedSfixed32); + extendableMessage.getExtension(RepeatedExtensions.repeatedFixed64); + extendableMessage.getExtension(RepeatedExtensions.repeatedSfixed64); + extendableMessage.getExtension(RepeatedExtensions.repeatedBool); + extendableMessage.getExtension(RepeatedExtensions.repeatedFloat); + extendableMessage.getExtension(RepeatedExtensions.repeatedDouble); + extendableMessage.getExtension(RepeatedExtensions.repeatedEnum); + extendableMessage.getExtension(RepeatedExtensions.repeatedString); + extendableMessage.getExtension(RepeatedExtensions.repeatedBytes); + extendableMessage.getExtension(RepeatedExtensions.repeatedMessage); + extendableMessage.getExtension(RepeatedExtensions.repeatedGroup); + assertEquals(extendableMessage, messageCopy); + assertEquals(extendableMessage.hashCode(), messageCopy.hashCode()); + + // Both deserialized. + messageCopy.getExtension(SingularExtensions.someInt32); + messageCopy.getExtension(RepeatedExtensions.repeatedUint32); + messageCopy.getExtension(RepeatedExtensions.repeatedSint32); + messageCopy.getExtension(RepeatedExtensions.repeatedInt64); + messageCopy.getExtension(RepeatedExtensions.repeatedUint64); + messageCopy.getExtension(RepeatedExtensions.repeatedSint64); + messageCopy.getExtension(RepeatedExtensions.repeatedFixed32); + messageCopy.getExtension(RepeatedExtensions.repeatedSfixed32); + messageCopy.getExtension(RepeatedExtensions.repeatedFixed64); + messageCopy.getExtension(RepeatedExtensions.repeatedSfixed64); + messageCopy.getExtension(RepeatedExtensions.repeatedBool); + messageCopy.getExtension(RepeatedExtensions.repeatedFloat); + messageCopy.getExtension(RepeatedExtensions.repeatedDouble); + messageCopy.getExtension(RepeatedExtensions.repeatedEnum); + messageCopy.getExtension(RepeatedExtensions.repeatedString); + messageCopy.getExtension(RepeatedExtensions.repeatedBytes); + messageCopy.getExtension(RepeatedExtensions.repeatedMessage); + messageCopy.getExtension(RepeatedExtensions.repeatedGroup); + assertEquals(extendableMessage, messageCopy); + assertEquals(extendableMessage.hashCode(), messageCopy.hashCode()); + + // Change one, make sure they are still different. + messageCopy.getExtension(RepeatedExtensions.repeatedMessage)[0].string = "not empty"; + assertFalse(extendableMessage.equals(messageCopy)); + + // Even if the extension hasn't been deserialized. + extendableMessage = Extensions.ExtendableMessage.parseFrom(data); + assertFalse(extendableMessage.equals(messageCopy)); + } + + public void testExtensionsCaching() { + Extensions.ExtendableMessage extendableMessage = new Extensions.ExtendableMessage(); + extendableMessage.setExtension(SingularExtensions.someMessage, + new Extensions.AnotherMessage()); + assertSame("Consecutive calls to getExtensions should return the same object", + extendableMessage.getExtension(SingularExtensions.someMessage), + extendableMessage.getExtension(SingularExtensions.someMessage)); + } + public void testUnknownFields() throws Exception { // Check that we roundtrip (serialize and deserialize) unrecognized fields. AnotherMessage message = new AnotherMessage();