diff options
Diffstat (limited to 'java')
7 files changed, 725 insertions, 46 deletions
diff --git a/java/README.txt b/java/README.txt index 3a004c7e..976ec842 100644 --- a/java/README.txt +++ b/java/README.txt @@ -429,6 +429,7 @@ Except: - Similar rename from CodedOutputStreamMicro to CodedOutputByteBufferNano. - Repeated fields are in arrays, not ArrayList or Vector. +- Full support of serializing/deserializing repeated packed fields. - Unset messages/groups are null, not an immutable empty default instance. - Required fields are always serialized. diff --git a/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java b/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java new file mode 100644 index 00000000..066774d5 --- /dev/null +++ b/java/src/main/java/com/google/protobuf/nano/ExtendableMessageNano.java @@ -0,0 +1,70 @@ +// 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.util.ArrayList; +import java.util.List; + +/** + * Base class of those Protocol Buffer messages that need to store unknown fields, + * such as extensions. + */ +public abstract class ExtendableMessageNano extends MessageNano { + /** + * 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> unknownFieldData; + + @Override + public int getSerializedSize() { + int size = WireFormatNano.computeWireSize(unknownFieldData); + cachedSize = size; + return size; + } + + /** + * Gets the value stored in the specified extension of this message. + */ + public <T> T getExtension(Extension<T> extension) { + return WireFormatNano.getExtension(extension, unknownFieldData); + } + + /** + * Sets the value of the specified extension of this message. + */ + public <T> void setExtension(Extension<T> extension, T value) { + if (unknownFieldData == null) { + unknownFieldData = new ArrayList<UnknownFieldData>(); + } + WireFormatNano.setExtension(extension, value, unknownFieldData); + } +}
\ No newline at end of file diff --git a/java/src/main/java/com/google/protobuf/nano/InternalNano.java b/java/src/main/java/com/google/protobuf/nano/InternalNano.java index 49309516..ce4a206c 100644 --- a/java/src/main/java/com/google/protobuf/nano/InternalNano.java +++ b/java/src/main/java/com/google/protobuf/nano/InternalNano.java @@ -31,6 +31,7 @@ package com.google.protobuf.nano; import java.io.UnsupportedEncodingException; +import java.util.Arrays; /** * The classes contained within are used internally by the Protocol Buffer @@ -40,7 +41,10 @@ import java.io.UnsupportedEncodingException; * * @author kenton@google.com (Kenton Varda) */ -public class InternalNano { +public final class InternalNano { + + private InternalNano() {} + /** * Helper called by generated code to construct default values for string * fields. @@ -69,7 +73,7 @@ public class InternalNano { * converts from the generated string to the string we actually want. The * generated code calls this automatically. */ - public static final String stringDefaultValue(String bytes) { + public static String stringDefaultValue(String bytes) { try { return new String(bytes.getBytes("ISO-8859-1"), "UTF-8"); } catch (UnsupportedEncodingException e) { @@ -88,7 +92,7 @@ public class InternalNano { * In this case we only need the second of the two hacks -- allowing us to * embed raw bytes as a string literal with ISO-8859-1 encoding. */ - public static final byte[] bytesDefaultValue(String bytes) { + public static byte[] bytesDefaultValue(String bytes) { try { return bytes.getBytes("ISO-8859-1"); } catch (UnsupportedEncodingException e) { @@ -103,11 +107,215 @@ public class InternalNano { * Helper function to convert a string into UTF-8 while turning the * UnsupportedEncodingException to a RuntimeException. */ - public static final byte[] copyFromUtf8(final String text) { + public static byte[] copyFromUtf8(final String text) { try { return text.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException("UTF-8 not supported?"); } } + + /** + * Checks repeated int field equality; null-value and 0-length fields are + * considered equal. + */ + public static boolean equals(int[] field1, int[] field2) { + if (field1 == null || field1.length == 0) { + return field2 == null || field2.length == 0; + } else { + return Arrays.equals(field1, field2); + } + } + + /** + * Checks repeated long field equality; null-value and 0-length fields are + * considered equal. + */ + public static boolean equals(long[] field1, long[] field2) { + if (field1 == null || field1.length == 0) { + return field2 == null || field2.length == 0; + } else { + return Arrays.equals(field1, field2); + } + } + + /** + * Checks repeated float field equality; null-value and 0-length fields are + * considered equal. + */ + public static boolean equals(float[] field1, float[] field2) { + if (field1 == null || field1.length == 0) { + return field2 == null || field2.length == 0; + } else { + return Arrays.equals(field1, field2); + } + } + + /** + * Checks repeated double field equality; null-value and 0-length fields are + * considered equal. + */ + public static boolean equals(double[] field1, double[] field2) { + if (field1 == null || field1.length == 0) { + return field2 == null || field2.length == 0; + } else { + return Arrays.equals(field1, field2); + } + } + + /** + * Checks repeated boolean field equality; null-value and 0-length fields are + * considered equal. + */ + public static boolean equals(boolean[] field1, boolean[] field2) { + if (field1 == null || field1.length == 0) { + return field2 == null || field2.length == 0; + } else { + return Arrays.equals(field1, field2); + } + } + + /** + * Checks repeated bytes field equality. Only non-null elements are tested. + * Returns true if the two fields have the same sequence of non-null + * elements. Null-value fields and fields of any length with only null + * elements are considered equal. + */ + public static boolean equals(byte[][] field1, byte[][] field2) { + int index1 = 0; + int length1 = field1 == null ? 0 : field1.length; + int index2 = 0; + int length2 = field2 == null ? 0 : field2.length; + while (true) { + while (index1 < length1 && field1[index1] == null) { + index1++; + } + while (index2 < length2 && field2[index2] == null) { + index2++; + } + boolean atEndOf1 = index1 >= length1; + boolean atEndOf2 = index2 >= length2; + if (atEndOf1 && atEndOf2) { + // no more non-null elements to test in both arrays + return true; + } else if (atEndOf1 != atEndOf2) { + // one of the arrays have extra non-null elements + return false; + } else if (!Arrays.equals(field1[index1], field2[index2])) { + // element mismatch + return false; + } + index1++; + index2++; + } + } + + /** + * Checks repeated string/message field equality. Only non-null elements are + * tested. Returns true if the two fields have the same sequence of non-null + * elements. Null-value fields and fields of any length with only null + * elements are considered equal. + */ + public static boolean equals(Object[] field1, Object[] field2) { + int index1 = 0; + int length1 = field1 == null ? 0 : field1.length; + int index2 = 0; + int length2 = field2 == null ? 0 : field2.length; + while (true) { + while (index1 < length1 && field1[index1] == null) { + index1++; + } + while (index2 < length2 && field2[index2] == null) { + index2++; + } + boolean atEndOf1 = index1 >= length1; + boolean atEndOf2 = index2 >= length2; + if (atEndOf1 && atEndOf2) { + // no more non-null elements to test in both arrays + return true; + } else if (atEndOf1 != atEndOf2) { + // one of the arrays have extra non-null elements + return false; + } else if (!field1[index1].equals(field2[index2])) { + // element mismatch + return false; + } + index1++; + index2++; + } + } + + /** + * Computes the hash code of a repeated int field. Null-value and 0-length + * fields have the same hash code. + */ + public static int hashCode(int[] field) { + return field == null || field.length == 0 ? 0 : Arrays.hashCode(field); + } + + /** + * Computes the hash code of a repeated long field. Null-value and 0-length + * fields have the same hash code. + */ + public static int hashCode(long[] field) { + return field == null || field.length == 0 ? 0 : Arrays.hashCode(field); + } + + /** + * Computes the hash code of a repeated float field. Null-value and 0-length + * fields have the same hash code. + */ + public static int hashCode(float[] field) { + return field == null || field.length == 0 ? 0 : Arrays.hashCode(field); + } + + /** + * Computes the hash code of a repeated double field. Null-value and 0-length + * fields have the same hash code. + */ + public static int hashCode(double[] field) { + return field == null || field.length == 0 ? 0 : Arrays.hashCode(field); + } + + /** + * Computes the hash code of a repeated boolean field. Null-value and 0-length + * fields have the same hash code. + */ + public static int hashCode(boolean[] field) { + return field == null || field.length == 0 ? 0 : Arrays.hashCode(field); + } + + /** + * Computes the hash code of a repeated bytes field. Only the sequence of all + * non-null elements are used in the computation. Null-value fields and fields + * of any length with only null elements have the same hash code. + */ + public static int hashCode(byte[][] field) { + int result = 0; + for (int i = 0, size = field == null ? 0 : field.length; i < size; i++) { + byte[] element = field[i]; + if (element != null) { + result = 31 * result + Arrays.hashCode(element); + } + } + return result; + } + + /** + * Computes the hash code of a repeated string/message field. Only the + * sequence of all non-null elements are used in the computation. Null-value + * fields and fields of any length with only null elements have the same hash + * code. + */ + public static int hashCode(Object[] field) { + int result = 0; + for (int i = 0, size = field == null ? 0 : field.length; i < size; i++) { + Object element = field[i]; + if (element != null) { + result = 31 * result + element.hashCode(); + } + } + return result; + } + } 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 8d4ec39e..0cf8416a 100644 --- a/java/src/main/java/com/google/protobuf/nano/MessageNano.java +++ b/java/src/main/java/com/google/protobuf/nano/MessageNano.java @@ -38,6 +38,8 @@ import java.io.IOException; * @author wink@google.com Wink Saville */ public abstract class MessageNano { + protected int cachedSize = -1; + /** * Get the number of bytes required to encode this message. * Returns the cached size or calls getSerializedSize which @@ -45,14 +47,24 @@ public abstract class MessageNano { * so the size is only computed once. If a member is modified * then this could be stale call getSerializedSize if in doubt. */ - abstract public int getCachedSize(); + public int getCachedSize() { + if (cachedSize < 0) { + // getSerializedSize sets cachedSize + getSerializedSize(); + } + return cachedSize; + } /** * Computes the number of bytes required to encode this message. * The size is cached and the cached result can be retrieved * using getCachedSize(). */ - abstract public int getSerializedSize(); + public int getSerializedSize() { + // This is overridden if the generated message has serialized fields. + cachedSize = 0; + return 0; + } /** * Serializes the message and writes it to {@code output}. This does not @@ -127,7 +139,10 @@ public abstract class MessageNano { } /** - * Intended for debugging purposes only. It does not use ASCII protobuf formatting. + * Returns a string that is (mostly) compatible with ProtoBuffer's TextFormat. Note that groups + * (which are deprecated) are not serialized with the correct field name. + * + * <p>This is implemented using reflection, so it is not especially fast. */ @Override public String toString() { diff --git a/java/src/main/java/com/google/protobuf/nano/MessageNanoPrinter.java b/java/src/main/java/com/google/protobuf/nano/MessageNanoPrinter.java index 3a5ee7c1..d135a514 100644 --- a/java/src/main/java/com/google/protobuf/nano/MessageNanoPrinter.java +++ b/java/src/main/java/com/google/protobuf/nano/MessageNanoPrinter.java @@ -47,20 +47,22 @@ public final class MessageNanoPrinter { private static final int MAX_STRING_LEN = 200; /** - * Returns an text representation of a MessageNano suitable for debugging. + * Returns an text representation of a MessageNano suitable for debugging. The returned string + * is mostly compatible with Protocol Buffer's TextFormat (as provided by non-nano protocol + * buffers) -- groups (which are deprecated) are output with an underscore name (e.g. foo_bar + * instead of FooBar) and will thus not parse. * * <p>Employs Java reflection on the given object and recursively prints primitive fields, * groups, and messages.</p> */ public static <T extends MessageNano> String print(T message) { if (message == null) { - return "null"; + return ""; } StringBuffer buf = new StringBuffer(); try { - print(message.getClass().getSimpleName(), message.getClass(), message, - new StringBuffer(), buf); + print(null, message.getClass(), message, new StringBuffer(), buf); } catch (IllegalAccessException e) { return "Error printing proto: " + e.getMessage(); } @@ -70,21 +72,30 @@ public final class MessageNanoPrinter { /** * Function that will print the given message/class into the StringBuffer. * Meant to be called recursively. + * + * @param identifier the identifier to use, or {@code null} if this is the root message to + * print. + * @param clazz the class of {@code message}. + * @param message the value to print. May in fact be a primitive value or byte array and not a + * message. + * @param indentBuf the indentation each line should begin with. + * @param buf the output buffer. */ private static void print(String identifier, Class<?> clazz, Object message, StringBuffer indentBuf, StringBuffer buf) throws IllegalAccessException { - if (MessageNano.class.isAssignableFrom(clazz)) { - // Nano proto message - buf.append(indentBuf).append(identifier); - - // If null, just print it and return - if (message == null) { - buf.append(": ").append(message).append("\n"); - return; + if (message == null) { + // This can happen if... + // - we're about to print a message, String, or byte[], but it not present; + // - we're about to print a primitive, but "reftype" optional style is enabled, and + // the field is unset. + // In both cases the appropriate behavior is to output nothing. + } else if (MessageNano.class.isAssignableFrom(clazz)) { // Nano proto message + int origIndentBufLength = indentBuf.length(); + if (identifier != null) { + buf.append(indentBuf).append(deCamelCaseify(identifier)).append(" <\n"); + indentBuf.append(INDENT); } - indentBuf.append(INDENT); - buf.append(" <\n"); for (Field field : clazz.getFields()) { // Proto fields are public, non-static variables that do not begin or end with '_' int modifiers = field.getModifiers(); @@ -115,15 +126,19 @@ public final class MessageNanoPrinter { print(fieldName, fieldType, value, indentBuf, buf); } } - indentBuf.delete(indentBuf.length() - INDENT.length(), indentBuf.length()); - buf.append(indentBuf).append(">\n"); + if (identifier != null) { + indentBuf.setLength(origIndentBufLength); + buf.append(indentBuf).append(">\n"); + } } else { - // Primitive value + // Non-null primitive value identifier = deCamelCaseify(identifier); buf.append(indentBuf).append(identifier).append(": "); if (message instanceof String) { String stringMessage = sanitizeString((String) message); buf.append("\"").append(stringMessage).append("\""); + } else if (message instanceof byte[]) { + appendQuotedBytes((byte[]) message, buf); } else { buf.append(message); } @@ -176,4 +191,27 @@ public final class MessageNanoPrinter { } return b.toString(); } + + /** + * Appends a quoted byte array to the provided {@code StringBuffer}. + */ + private static void appendQuotedBytes(byte[] bytes, StringBuffer builder) { + if (bytes == null) { + builder.append("\"\""); + return; + } + + builder.append('"'); + for (int i = 0; i < bytes.length; ++i) { + int ch = bytes[i]; + if (ch == '\\' || ch == '"') { + builder.append('\\').append((char) ch); + } else if (ch >= 32 && ch < 127) { + builder.append((char) ch); + } else { + builder.append(String.format("\\%03o", ch)); + } + } + builder.append('"'); + } } 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 0db2a83c..833ed2a9 100644 --- a/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java +++ b/java/src/main/java/com/google/protobuf/nano/UnknownFieldData.java @@ -30,6 +30,8 @@ package com.google.protobuf.nano; +import java.util.Arrays; + /** * Stores unknown fields. These might be extensions or fields that the generated API doesn't * know about yet. @@ -37,6 +39,7 @@ package com.google.protobuf.nano; * @author bduff@google.com (Brian Duff) */ public final class UnknownFieldData { + final int tag; final byte[] bytes; @@ -44,4 +47,25 @@ public final class UnknownFieldData { this.tag = tag; this.bytes = bytes; } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof UnknownFieldData)) { + return false; + } + + 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 1149c40b..0e1c9bc9 100644 --- a/java/src/test/java/com/google/protobuf/NanoTest.java +++ b/java/src/test/java/com/google/protobuf/NanoTest.java @@ -49,6 +49,7 @@ import com.google.protobuf.nano.NanoHasOuterClass.TestAllTypesNanoHas; import com.google.protobuf.nano.NanoOuterClass; import com.google.protobuf.nano.NanoOuterClass.TestAllTypesNano; import com.google.protobuf.nano.NanoReferenceTypes; +import com.google.protobuf.nano.NanoRepeatedPackables; import com.google.protobuf.nano.TestRepeatedMergeNano; import com.google.protobuf.nano.UnittestImportNano; import com.google.protobuf.nano.UnittestMultipleNano; @@ -60,6 +61,7 @@ import junit.framework.TestCase; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; /** @@ -2489,14 +2491,14 @@ public class NanoTest extends TestCase { msg.optionalInt32 = 14; msg.optionalFloat = 42.3f; msg.optionalString = "String \"with' both quotes"; - msg.optionalBytes = new byte[5]; + msg.optionalBytes = new byte[] {'"', '\0', 1, 8}; msg.optionalGroup = new TestAllTypesNano.OptionalGroup(); msg.optionalGroup.a = 15; msg.repeatedInt64 = new long[2]; msg.repeatedInt64[0] = 1L; msg.repeatedInt64[1] = -1L; msg.repeatedBytes = new byte[2][]; - msg.repeatedBytes[1] = new byte[5]; + msg.repeatedBytes[1] = new byte[] {'h', 'e', 'l', 'l', 'o'}; msg.repeatedGroup = new TestAllTypesNano.RepeatedGroup[2]; msg.repeatedGroup[0] = new TestAllTypesNano.RepeatedGroup(); msg.repeatedGroup[0].a = -27; @@ -2513,28 +2515,31 @@ public class NanoTest extends TestCase { msg.repeatedNestedEnum = new int[2]; msg.repeatedNestedEnum[0] = TestAllTypesNano.BAR; msg.repeatedNestedEnum[1] = TestAllTypesNano.FOO; + msg.repeatedStringPiece = new String[] {null, "world"}; String protoPrint = msg.toString(); - assertTrue(protoPrint.contains("TestAllTypesNano <")); - assertTrue(protoPrint.contains(" optional_int32: 14")); - assertTrue(protoPrint.contains(" optional_float: 42.3")); - assertTrue(protoPrint.contains(" optional_double: 0.0")); - assertTrue(protoPrint.contains(" optional_string: \"String \\u0022with\\u0027 both quotes\"")); - assertTrue(protoPrint.contains(" optional_bytes: [B@")); - assertTrue(protoPrint.contains(" optionalGroup <\n a: 15\n >")); - - assertTrue(protoPrint.contains(" repeated_int64: 1")); - assertTrue(protoPrint.contains(" repeated_int64: -1")); - assertTrue(protoPrint.contains(" repeated_bytes: null\n repeated_bytes: [B@")); - assertTrue(protoPrint.contains(" repeatedGroup <\n a: -27\n >\n" - + " repeatedGroup <\n a: -72\n >")); - assertTrue(protoPrint.contains(" optionalNestedMessage <\n bb: 7\n >")); - assertTrue(protoPrint.contains(" repeatedNestedMessage <\n bb: 77\n >\n" - + " repeatedNestedMessage <\n bb: 88\n >")); - assertTrue(protoPrint.contains(" optional_nested_enum: 3")); - assertTrue(protoPrint.contains(" repeated_nested_enum: 2\n repeated_nested_enum: 1")); - assertTrue(protoPrint.contains(" default_int32: 41")); - assertTrue(protoPrint.contains(" default_string: \"hello\"")); + assertTrue(protoPrint.contains("optional_int32: 14")); + assertTrue(protoPrint.contains("optional_float: 42.3")); + assertTrue(protoPrint.contains("optional_double: 0.0")); + assertTrue(protoPrint.contains("optional_string: \"String \\u0022with\\u0027 both quotes\"")); + assertTrue(protoPrint.contains("optional_bytes: \"\\\"\\000\\001\\010\"")); + assertTrue(protoPrint.contains("optional_group <\n a: 15\n>")); + + assertTrue(protoPrint.contains("repeated_int64: 1")); + assertTrue(protoPrint.contains("repeated_int64: -1")); + assertFalse(protoPrint.contains("repeated_bytes: \"\"")); // null should be dropped + assertTrue(protoPrint.contains("repeated_bytes: \"hello\"")); + assertTrue(protoPrint.contains("repeated_group <\n a: -27\n>\n" + + "repeated_group <\n a: -72\n>")); + assertTrue(protoPrint.contains("optional_nested_message <\n bb: 7\n>")); + assertTrue(protoPrint.contains("repeated_nested_message <\n bb: 77\n>\n" + + "repeated_nested_message <\n bb: 88\n>")); + assertTrue(protoPrint.contains("optional_nested_enum: 3")); + assertTrue(protoPrint.contains("repeated_nested_enum: 2\nrepeated_nested_enum: 1")); + assertTrue(protoPrint.contains("default_int32: 41")); + assertTrue(protoPrint.contains("default_string: \"hello\"")); + assertFalse(protoPrint.contains("repeated_string_piece: \"\"")); // null should be dropped + assertTrue(protoPrint.contains("repeated_string_piece: \"world\"")); } public void testExtensions() throws Exception { @@ -2684,6 +2689,222 @@ public class NanoTest extends TestCase { assertHasWireData(message, false); } + public void testHashCodeEquals() throws Exception { + // Complete equality: + TestAllTypesNano a = createMessageForHashCodeEqualsTest(); + TestAllTypesNano aEquivalent = createMessageForHashCodeEqualsTest(); + + // Null and empty array for repeated fields equality: + TestAllTypesNano b = createMessageForHashCodeEqualsTest(); + b.repeatedBool = null; + b.repeatedFloat = new float[0]; + TestAllTypesNano bEquivalent = createMessageForHashCodeEqualsTest(); + bEquivalent.repeatedBool = new boolean[0]; + bEquivalent.repeatedFloat = null; + + // Ref-element-type repeated fields use non-null subsequence equality: + TestAllTypesNano c = createMessageForHashCodeEqualsTest(); + c.repeatedString = null; + c.repeatedStringPiece = new String[] {null, "one", null, "two"}; + c.repeatedBytes = new byte[][] {{3, 4}, null}; + TestAllTypesNano cEquivalent = createMessageForHashCodeEqualsTest(); + cEquivalent.repeatedString = new String[3]; + cEquivalent.repeatedStringPiece = new String[] {"one", "two", null}; + cEquivalent.repeatedBytes = new byte[][] {{3, 4}}; + + // Complete equality for messages with has fields: + TestAllTypesNanoHas d = createMessageWithHasForHashCodeEqualsTest(); + TestAllTypesNanoHas dEquivalent = createMessageWithHasForHashCodeEqualsTest(); + + // If has-fields exist, fields with the same default values but + // different has-field values are different. + TestAllTypesNanoHas e = createMessageWithHasForHashCodeEqualsTest(); + e.optionalInt32++; // make different from d + e.hasDefaultString = false; + TestAllTypesNanoHas eDifferent = createMessageWithHasForHashCodeEqualsTest(); + eDifferent.optionalInt32 = e.optionalInt32; + eDifferent.hasDefaultString = true; + + // Complete equality for messages with accessors: + TestNanoAccessors f = createMessageWithAccessorsForHashCodeEqualsTest(); + TestNanoAccessors fEquivalent = createMessageWithAccessorsForHashCodeEqualsTest(); + + // If using accessors, explicitly setting a field to its default value + // should make the message different. + TestNanoAccessors g = createMessageWithAccessorsForHashCodeEqualsTest(); + g.setOptionalInt32(g.getOptionalInt32() + 1); // make different from f + g.clearDefaultString(); + TestNanoAccessors gDifferent = createMessageWithAccessorsForHashCodeEqualsTest(); + gDifferent.setOptionalInt32(g.getOptionalInt32()); + gDifferent.setDefaultString(g.getDefaultString()); + + // Complete equality for reference typed messages: + NanoReferenceTypes.TestAllTypesNano h = createRefTypedMessageForHashCodeEqualsTest(); + NanoReferenceTypes.TestAllTypesNano hEquivalent = createRefTypedMessageForHashCodeEqualsTest(); + + // Inequality of null and default value for reference typed messages: + NanoReferenceTypes.TestAllTypesNano i = createRefTypedMessageForHashCodeEqualsTest(); + i.optionalInt32 = 1; // make different from h + i.optionalFloat = null; + NanoReferenceTypes.TestAllTypesNano iDifferent = createRefTypedMessageForHashCodeEqualsTest(); + iDifferent.optionalInt32 = i.optionalInt32; + iDifferent.optionalFloat = 0.0f; + + HashMap<MessageNano, String> hashMap = new HashMap<MessageNano, String>(); + hashMap.put(a, "a"); + hashMap.put(b, "b"); + hashMap.put(c, "c"); + hashMap.put(d, "d"); + hashMap.put(e, "e"); + hashMap.put(f, "f"); + hashMap.put(g, "g"); + hashMap.put(h, "h"); + hashMap.put(i, "i"); + + assertEquals(9, hashMap.size()); // a-i should be different from each other. + + assertEquals("a", hashMap.get(a)); + assertEquals("a", hashMap.get(aEquivalent)); + + assertEquals("b", hashMap.get(b)); + assertEquals("b", hashMap.get(bEquivalent)); + + assertEquals("c", hashMap.get(c)); + assertEquals("c", hashMap.get(cEquivalent)); + + assertEquals("d", hashMap.get(d)); + assertEquals("d", hashMap.get(dEquivalent)); + + assertEquals("e", hashMap.get(e)); + assertNull(hashMap.get(eDifferent)); + + assertEquals("f", hashMap.get(f)); + assertEquals("f", hashMap.get(fEquivalent)); + + assertEquals("g", hashMap.get(g)); + assertNull(hashMap.get(gDifferent)); + + assertEquals("h", hashMap.get(h)); + assertEquals("h", hashMap.get(hEquivalent)); + + assertEquals("i", hashMap.get(i)); + assertNull(hashMap.get(iDifferent)); + } + + private TestAllTypesNano createMessageForHashCodeEqualsTest() { + TestAllTypesNano message = new TestAllTypesNano(); + message.optionalInt32 = 5; + message.optionalInt64 = 777; + message.optionalFloat = 1.0f; + message.optionalDouble = 2.0; + message.optionalBool = true; + message.optionalString = "Hello"; + message.optionalBytes = new byte[] { 1, 2, 3 }; + message.optionalNestedMessage = new TestAllTypesNano.NestedMessage(); + message.optionalNestedMessage.bb = 27; + message.optionalNestedEnum = TestAllTypesNano.BAR; + message.repeatedInt32 = new int[] { 5, 6, 7, 8 }; + message.repeatedInt64 = new long[] { 27L, 28L, 29L }; + message.repeatedFloat = new float[] { 5.0f, 6.0f }; + message.repeatedDouble = new double[] { 99.1, 22.5 }; + message.repeatedBool = new boolean[] { true, false, true }; + message.repeatedString = new String[] { "One", "Two" }; + message.repeatedBytes = new byte[][] { { 2, 7 }, { 2, 7 } }; + message.repeatedNestedMessage = new TestAllTypesNano.NestedMessage[] { + message.optionalNestedMessage, + message.optionalNestedMessage + }; + message.repeatedNestedEnum = new int[] { + TestAllTypesNano.BAR, + TestAllTypesNano.BAZ + }; + // We set the _nan fields to something other than nan, because equality + // is defined for nan such that Float.NaN != Float.NaN, which makes any + // instance of TestAllTypesNano unequal to any other instance unless + // these fields are set. This is also the behavior of the regular java + // generator when the value of a field is NaN. + message.defaultFloatNan = 1.0f; + message.defaultDoubleNan = 1.0; + return message; + } + + private TestAllTypesNanoHas createMessageWithHasForHashCodeEqualsTest() { + TestAllTypesNanoHas message = new TestAllTypesNanoHas(); + message.optionalInt32 = 5; + message.optionalString = "Hello"; + message.optionalBytes = new byte[] { 1, 2, 3 }; + message.optionalNestedMessage = new TestAllTypesNanoHas.NestedMessage(); + message.optionalNestedMessage.bb = 27; + message.optionalNestedEnum = TestAllTypesNano.BAR; + message.repeatedInt32 = new int[] { 5, 6, 7, 8 }; + message.repeatedString = new String[] { "One", "Two" }; + message.repeatedBytes = new byte[][] { { 2, 7 }, { 2, 7 } }; + message.repeatedNestedMessage = new TestAllTypesNanoHas.NestedMessage[] { + message.optionalNestedMessage, + message.optionalNestedMessage + }; + message.repeatedNestedEnum = new int[] { + TestAllTypesNano.BAR, + TestAllTypesNano.BAZ + }; + message.defaultFloatNan = 1.0f; + return message; + } + + private TestNanoAccessors createMessageWithAccessorsForHashCodeEqualsTest() { + TestNanoAccessors message = new TestNanoAccessors() + .setOptionalInt32(5) + .setOptionalString("Hello") + .setOptionalBytes(new byte[] {1, 2, 3}) + .setOptionalNestedMessage(new TestNanoAccessors.NestedMessage().setBb(27)) + .setOptionalNestedEnum(TestNanoAccessors.BAR) + .setDefaultFloatNan(1.0f); + message.repeatedInt32 = new int[] { 5, 6, 7, 8 }; + message.repeatedString = new String[] { "One", "Two" }; + message.repeatedBytes = new byte[][] { { 2, 7 }, { 2, 7 } }; + message.repeatedNestedMessage = new TestNanoAccessors.NestedMessage[] { + message.getOptionalNestedMessage(), + message.getOptionalNestedMessage() + }; + message.repeatedNestedEnum = new int[] { + TestAllTypesNano.BAR, + TestAllTypesNano.BAZ + }; + return message; + } + + private NanoReferenceTypes.TestAllTypesNano createRefTypedMessageForHashCodeEqualsTest() { + NanoReferenceTypes.TestAllTypesNano message = new NanoReferenceTypes.TestAllTypesNano(); + message.optionalInt32 = 5; + message.optionalInt64 = 777L; + message.optionalFloat = 1.0f; + message.optionalDouble = 2.0; + message.optionalBool = true; + message.optionalString = "Hello"; + message.optionalBytes = new byte[] { 1, 2, 3 }; + message.optionalNestedMessage = + new NanoReferenceTypes.TestAllTypesNano.NestedMessage(); + message.optionalNestedMessage.foo = 27; + message.optionalNestedEnum = NanoReferenceTypes.TestAllTypesNano.BAR; + message.repeatedInt32 = new int[] { 5, 6, 7, 8 }; + message.repeatedInt64 = new long[] { 27L, 28L, 29L }; + message.repeatedFloat = new float[] { 5.0f, 6.0f }; + message.repeatedDouble = new double[] { 99.1, 22.5 }; + message.repeatedBool = new boolean[] { true, false, true }; + message.repeatedString = new String[] { "One", "Two" }; + message.repeatedBytes = new byte[][] { { 2, 7 }, { 2, 7 } }; + message.repeatedNestedMessage = + new NanoReferenceTypes.TestAllTypesNano.NestedMessage[] { + message.optionalNestedMessage, + message.optionalNestedMessage + }; + message.repeatedNestedEnum = new int[] { + NanoReferenceTypes.TestAllTypesNano.BAR, + NanoReferenceTypes.TestAllTypesNano.BAZ + }; + return message; + } + public void testNullRepeatedFields() throws Exception { // Check that serialization after explicitly setting a repeated field // to null doesn't NPE. @@ -2746,6 +2967,37 @@ public class NanoTest extends TestCase { assertEquals(TestAllTypesNano.BAR, message.repeatedPackedNestedEnum[1]); } + public void testNullRepeatedFieldElements() throws Exception { + // Check that serialization with null array elements doesn't NPE. + String string1 = "1"; + String string2 = "2"; + byte[] bytes1 = {3, 4}; + byte[] bytes2 = {5, 6}; + TestAllTypesNano.NestedMessage msg1 = new TestAllTypesNano.NestedMessage(); + msg1.bb = 7; + TestAllTypesNano.NestedMessage msg2 = new TestAllTypesNano.NestedMessage(); + msg2.bb = 8; + + TestAllTypesNano message = new TestAllTypesNano(); + message.repeatedString = new String[] {null, string1, string2}; + message.repeatedBytes = new byte[][] {bytes1, null, bytes2}; + message.repeatedNestedMessage = new TestAllTypesNano.NestedMessage[] {msg1, msg2, null}; + message.repeatedGroup = new TestAllTypesNano.RepeatedGroup[] {null, null, null}; + + byte[] serialized = MessageNano.toByteArray(message); // should not NPE + TestAllTypesNano deserialized = MessageNano.mergeFrom(new TestAllTypesNano(), serialized); + assertEquals(2, deserialized.repeatedString.length); + assertEquals(string1, deserialized.repeatedString[0]); + assertEquals(string2, deserialized.repeatedString[1]); + assertEquals(2, deserialized.repeatedBytes.length); + assertTrue(Arrays.equals(bytes1, deserialized.repeatedBytes[0])); + assertTrue(Arrays.equals(bytes2, deserialized.repeatedBytes[1])); + assertEquals(2, deserialized.repeatedNestedMessage.length); + assertEquals(msg1.bb, deserialized.repeatedNestedMessage[0].bb); + assertEquals(msg2.bb, deserialized.repeatedNestedMessage[1].bb); + assertEquals(0, deserialized.repeatedGroup.length); + } + public void testRepeatedMerge() throws Exception { // Check that merging repeated fields cause the arrays to expand with // new data. @@ -2813,6 +3065,77 @@ public class NanoTest extends TestCase { assertEquals(30, firstContainer.contained.repeatedInt32[2]); } + public void testRepeatedPackables() throws Exception { + // Check that repeated fields with packable types can accept both packed and unpacked + // serialized forms. + NanoRepeatedPackables.NonPacked nonPacked = new NanoRepeatedPackables.NonPacked(); + nonPacked.int32S = new int[] {1, 2, 3}; + nonPacked.int64S = new long[] {4, 5, 6}; + nonPacked.uint32S = new int[] {7, 8, 9}; + nonPacked.uint64S = new long[] {10, 11, 12}; + nonPacked.sint32S = new int[] {13, 14, 15}; + nonPacked.sint64S = new long[] {16, 17, 18}; + nonPacked.fixed32S = new int[] {19, 20, 21}; + nonPacked.fixed64S = new long[] {22, 23, 24}; + nonPacked.sfixed32S = new int[] {25, 26, 27}; + nonPacked.sfixed64S = new long[] {28, 29, 30}; + nonPacked.floats = new float[] {31, 32, 33}; + nonPacked.doubles = new double[] {34, 35, 36}; + nonPacked.bools = new boolean[] {false, true}; + nonPacked.enums = new int[] { + NanoRepeatedPackables.Enum.OPTION_ONE, + NanoRepeatedPackables.Enum.OPTION_TWO, + }; + nonPacked.noise = 13579; + + byte[] nonPackedSerialized = MessageNano.toByteArray(nonPacked); + + NanoRepeatedPackables.Packed packed = + MessageNano.mergeFrom(new NanoRepeatedPackables.Packed(), nonPackedSerialized); + assertRepeatedPackablesEqual(nonPacked, packed); + + byte[] packedSerialized = MessageNano.toByteArray(packed); + // Just a cautious check that the two serialized forms are different, + // to make sure the remaining of this test is useful: + assertFalse(Arrays.equals(nonPackedSerialized, packedSerialized)); + + nonPacked = MessageNano.mergeFrom(new NanoRepeatedPackables.NonPacked(), packedSerialized); + assertRepeatedPackablesEqual(nonPacked, packed); + + // Test mixed serialized form. + byte[] mixedSerialized = new byte[nonPackedSerialized.length + packedSerialized.length]; + System.arraycopy(nonPackedSerialized, 0, mixedSerialized, 0, nonPackedSerialized.length); + System.arraycopy(packedSerialized, 0, + mixedSerialized, nonPackedSerialized.length, packedSerialized.length); + + nonPacked = MessageNano.mergeFrom(new NanoRepeatedPackables.NonPacked(), mixedSerialized); + packed = MessageNano.mergeFrom(new NanoRepeatedPackables.Packed(), mixedSerialized); + assertRepeatedPackablesEqual(nonPacked, packed); + assertTrue(Arrays.equals(new int[] {1, 2, 3, 1, 2, 3}, nonPacked.int32S)); + assertTrue(Arrays.equals(new int[] {13, 14, 15, 13, 14, 15}, nonPacked.sint32S)); + assertTrue(Arrays.equals(new int[] {25, 26, 27, 25, 26, 27}, nonPacked.sfixed32S)); + assertTrue(Arrays.equals(new boolean[] {false, true, false, true}, nonPacked.bools)); + } + + private void assertRepeatedPackablesEqual( + NanoRepeatedPackables.NonPacked nonPacked, NanoRepeatedPackables.Packed packed) { + // Not using MessageNano.equals() -- that belongs to a separate test. + assertTrue(Arrays.equals(nonPacked.int32S, packed.int32S)); + assertTrue(Arrays.equals(nonPacked.int64S, packed.int64S)); + assertTrue(Arrays.equals(nonPacked.uint32S, packed.uint32S)); + assertTrue(Arrays.equals(nonPacked.uint64S, packed.uint64S)); + assertTrue(Arrays.equals(nonPacked.sint32S, packed.sint32S)); + assertTrue(Arrays.equals(nonPacked.sint64S, packed.sint64S)); + assertTrue(Arrays.equals(nonPacked.fixed32S, packed.fixed32S)); + assertTrue(Arrays.equals(nonPacked.fixed64S, packed.fixed64S)); + assertTrue(Arrays.equals(nonPacked.sfixed32S, packed.sfixed32S)); + assertTrue(Arrays.equals(nonPacked.sfixed64S, packed.sfixed64S)); + assertTrue(Arrays.equals(nonPacked.floats, packed.floats)); + assertTrue(Arrays.equals(nonPacked.doubles, packed.doubles)); + assertTrue(Arrays.equals(nonPacked.bools, packed.bools)); + assertTrue(Arrays.equals(nonPacked.enums, packed.enums)); + } + private void assertHasWireData(MessageNano message, boolean expected) { byte[] bytes = MessageNano.toByteArray(message); int wireLength = bytes.length; |