From 9432d8bee4016912c4dcba5b83a76f5fe21a1487 Mon Sep 17 00:00:00 2001 From: Mairbek Khadikov Date: Tue, 6 Jun 2017 15:20:03 -0700 Subject: [PATCH] Cloud Spanner: Make Spanner value classes serializable (#2040) * Make struct serializable * Make Statement and TimestampBound serializable * Make SessionPoolOptions serializable --- .../com/google/cloud/spanner/SpannerImpl.java | 342 +++++++++++------- .../com/google/cloud/spanner/Statement.java | 6 +- .../java/com/google/cloud/spanner/Struct.java | 4 +- .../google/cloud/spanner/TimestampBound.java | 5 +- .../java/com/google/cloud/spanner/Type.java | 4 +- .../cloud/spanner/GrpcResultSetTest.java | 127 ++++++- .../google/cloud/spanner/StatementTest.java | 16 + .../cloud/spanner/TimestampBoundTest.java | 10 + .../com/google/cloud/spanner/ValueTest.java | 59 ++- 9 files changed, 396 insertions(+), 177 deletions(-) diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java index debe2bbd979c..885723109ccd 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerImpl.java @@ -77,6 +77,7 @@ import io.grpc.Context; import io.grpc.ManagedChannel; import java.io.IOException; +import java.io.Serializable; import java.util.AbstractList; import java.util.ArrayList; import java.util.BitSet; @@ -1497,142 +1498,6 @@ public Transaction call() throws Exception { } } - /** - * Base class for gRPC/proto-based structs. - * - * @param the type of row data - */ - private static class BaseStruct extends Struct { - protected final Type type; - protected final List rowData; - - private BaseStruct(Type type, List rowData) { - this.type = type; - this.rowData = rowData; - } - - Struct immutableCopy() { - return new BaseStruct(type, new ArrayList<>(rowData)); - } - - @Override - public Type getType() { - return type; - } - - @Override - public boolean isNull(int columnIndex) { - return rowData.get(columnIndex) == null; - } - - @Override - protected boolean getBooleanInternal(int columnIndex) { - return (Boolean) rowData.get(columnIndex); - } - - @Override - protected long getLongInternal(int columnIndex) { - return (Long) rowData.get(columnIndex); - } - - @Override - protected double getDoubleInternal(int columnIndex) { - return (Double) rowData.get(columnIndex); - } - - @Override - protected String getStringInternal(int columnIndex) { - return (String) rowData.get(columnIndex); - } - - @Override - protected ByteArray getBytesInternal(int columnIndex) { - return (ByteArray) rowData.get(columnIndex); - } - - @Override - protected Timestamp getTimestampInternal(int columnIndex) { - return (Timestamp) rowData.get(columnIndex); - } - - @Override - protected Date getDateInternal(int columnIndex) { - return (Date) rowData.get(columnIndex); - } - - @Override - protected boolean[] getBooleanArrayInternal(int columnIndex) { - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - List values = (List) rowData.get(columnIndex); - boolean[] r = new boolean[values.size()]; - for (int i = 0; i < values.size(); ++i) { - if (values.get(i) == null) { - throw throwNotNull(columnIndex); - } - r[i] = values.get(i); - } - return r; - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getBooleanListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - protected long[] getLongArrayInternal(int columnIndex) { - return getLongListInternal(columnIndex).toPrimitiveArray(columnIndex); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces an Int64Array. - protected Int64Array getLongListInternal(int columnIndex) { - return (Int64Array) rowData.get(columnIndex); - } - - @Override - protected double[] getDoubleArrayInternal(int columnIndex) { - return getDoubleListInternal(columnIndex).toPrimitiveArray(columnIndex); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a Float64Array. - protected Float64Array getDoubleListInternal(int columnIndex) { - return (Float64Array) rowData.get(columnIndex); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getStringListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getBytesListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getTimestampListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY produces a List. - protected List getDateListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - - @Override - @SuppressWarnings("unchecked") // We know ARRAY> produces a List. - protected List getStructListInternal(int columnIndex) { - return Collections.unmodifiableList((List) rowData.get(columnIndex)); - } - } - @VisibleForTesting abstract static class AbstractResultSet extends AbstractStructReader implements ResultSet { interface Listener { @@ -1649,7 +1514,7 @@ interface Listener { void onDone(); } - protected abstract BaseStruct currRow(); + protected abstract GrpcStruct currRow(); @Override public Struct getCurrentRowAsStruct() { @@ -1770,7 +1635,7 @@ static class GrpcResultSet extends AbstractResultSet> { } @Override - protected BaseStruct> currRow() { + protected GrpcStruct currRow() { checkState(!closed, "ResultSet is closed"); checkState(currRow != null, "next() call required"); return currRow; @@ -1829,9 +1694,85 @@ private SpannerException yieldError(SpannerException e) { } } - private static class GrpcStruct extends BaseStruct> { + private static class GrpcStruct extends Struct implements Serializable { + + protected final Type type; + protected final List rowData; + + /** + * Builds an immutable version of this struct using {@link Struct#newBuilder()} which is used + * as a serialization proxy. + */ + private Object writeReplace() { + Builder builder = Struct.newBuilder(); + List structFields = getType().getStructFields(); + for (int i = 0; i < structFields.size(); i++) { + Type.StructField field = structFields.get(i); + String fieldName = field.getName(); + Object value = rowData.get(i); + Type fieldType = field.getType(); + switch (fieldType.getCode()) { + case BOOL: + builder.set(fieldName).to((Boolean) value); + break; + case INT64: + builder.set(fieldName).to((Long) value); + break; + case FLOAT64: + builder.set(fieldName).to((Double) value); + break; + case STRING: + builder.set(fieldName).to((String) value); + break; + case BYTES: + builder.set(fieldName).to((ByteArray) value); + break; + case TIMESTAMP: + builder.set(fieldName).to((Timestamp) value); + break; + case DATE: + builder.set(fieldName).to((Date) value); + break; + case ARRAY: + switch(fieldType.getArrayElementType().getCode()) { + case BOOL: + builder.set(fieldName).toBoolArray((Iterable) value); + break; + case INT64: + builder.set(fieldName).toInt64Array((Iterable) value); + break; + case FLOAT64: + builder.set(fieldName).toFloat64Array((Iterable) value); + break; + case STRING: + builder.set(fieldName).toStringArray((Iterable) value); + break; + case BYTES: + builder.set(fieldName).toBytesArray((Iterable) value); + break; + case TIMESTAMP: + builder.set(fieldName).toTimestampArray((Iterable) value); + break; + case DATE: + builder.set(fieldName).toDateArray((Iterable) value); + break; + case STRUCT: + builder.add(fieldName, fieldType.getArrayElementType().getStructFields(), (Iterable) value); + break; + } + break; + case STRUCT: // Not a legal top-level field type. + default: + throw new AssertionError("Unhandled type code: " + fieldType.getCode()); + } + + } + return builder.build(); + } + GrpcStruct(Type type, List rowData) { - super(type, rowData); + this.type = type; + this.rowData = rowData; } boolean consumeRow(Iterator iterator) { @@ -1989,6 +1930,127 @@ private static void checkType( + proto.getKindCase()); } } + + Struct immutableCopy() { + return new GrpcStruct(type, new ArrayList<>(rowData)); + } + + @Override + public Type getType() { + return type; + } + + @Override + public boolean isNull(int columnIndex) { + return rowData.get(columnIndex) == null; + } + + @Override + protected boolean getBooleanInternal(int columnIndex) { + return (Boolean) rowData.get(columnIndex); + } + + @Override + protected long getLongInternal(int columnIndex) { + return (Long) rowData.get(columnIndex); + } + + @Override + protected double getDoubleInternal(int columnIndex) { + return (Double) rowData.get(columnIndex); + } + + @Override + protected String getStringInternal(int columnIndex) { + return (String) rowData.get(columnIndex); + } + + @Override + protected ByteArray getBytesInternal(int columnIndex) { + return (ByteArray) rowData.get(columnIndex); + } + + @Override + protected Timestamp getTimestampInternal(int columnIndex) { + return (Timestamp) rowData.get(columnIndex); + } + + @Override + protected Date getDateInternal(int columnIndex) { + return (Date) rowData.get(columnIndex); + } + + @Override + protected boolean[] getBooleanArrayInternal(int columnIndex) { + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + List values = (List) rowData.get(columnIndex); + boolean[] r = new boolean[values.size()]; + for (int i = 0; i < values.size(); ++i) { + if (values.get(i) == null) { + throw throwNotNull(columnIndex); + } + r[i] = values.get(i); + } + return r; + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getBooleanListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + protected long[] getLongArrayInternal(int columnIndex) { + return getLongListInternal(columnIndex).toPrimitiveArray(columnIndex); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces an Int64Array. + protected Int64Array getLongListInternal(int columnIndex) { + return (Int64Array) rowData.get(columnIndex); + } + + @Override + protected double[] getDoubleArrayInternal(int columnIndex) { + return getDoubleListInternal(columnIndex).toPrimitiveArray(columnIndex); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a Float64Array. + protected Float64Array getDoubleListInternal(int columnIndex) { + return (Float64Array) rowData.get(columnIndex); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getStringListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getBytesListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getTimestampListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY produces a List. + protected List getDateListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } + + @Override + @SuppressWarnings("unchecked") // We know ARRAY> produces a List. + protected List getStructListInternal(int columnIndex) { + return Collections.unmodifiableList((List) rowData.get(columnIndex)); + } } @VisibleForTesting diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java index ac12640a5000..02cdc0280964 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Statement.java @@ -21,6 +21,8 @@ import com.google.cloud.spanner.ReadContext.QueryAnalyzeMode; import com.google.common.collect.ImmutableMap; + +import java.io.Serializable; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -50,7 +52,9 @@ * *

{@code Statement} instances are immutable. */ -public final class Statement { +public final class Statement implements Serializable { + private static final long serialVersionUID = -1967958247625065259L; + private final ImmutableMap parameters; private final String sql; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java index 276514840a87..814cdce1dd2b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Struct.java @@ -26,6 +26,8 @@ import com.google.common.primitives.Booleans; import com.google.common.primitives.Doubles; import com.google.common.primitives.Longs; + +import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -41,7 +43,7 @@ *

{@code Struct} instances are immutable. */ @Immutable -public abstract class Struct extends AbstractStructReader { +public abstract class Struct extends AbstractStructReader implements Serializable { // Only implementations within the package are allowed. Struct() {} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TimestampBound.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TimestampBound.java index 432cf114ffbf..77e9c8dcb0ed 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TimestampBound.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TimestampBound.java @@ -24,6 +24,8 @@ import com.google.protobuf.Duration; import com.google.protobuf.util.Durations; import com.google.spanner.v1.TransactionOptions; + +import java.io.Serializable; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -115,10 +117,11 @@ * @see Session#singleUseReadOnlyTransaction(TimestampBound) * @see Session#readOnlyTransaction(TimestampBound) */ -public final class TimestampBound { +public final class TimestampBound implements Serializable { private static final TimestampBound STRONG_BOUND = new TimestampBound(Mode.STRONG, null, null); private static final TransactionOptions.ReadOnly STRONG_PROTO = TransactionOptions.ReadOnly.newBuilder().setStrong(true).build(); + private static final long serialVersionUID = 9194565742651275731L; private final Mode mode; private final Timestamp timestamp; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java index 42a0b6c679b9..22815e8a4d2b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/Type.java @@ -207,7 +207,9 @@ static Code fromProtoCode(TypeCode protoCode) { } /** Describes an individual field in a {@code STRUCT type}. */ - public static final class StructField { + public static final class StructField implements Serializable { + private static final long serialVersionUID = 8640511292704408210L; + private final String name; private final Type type; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java index e6e0e260516f..d2b589a7e9a6 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/GrpcResultSetTest.java @@ -17,11 +17,16 @@ package com.google.cloud.spanner; import static com.google.cloud.spanner.SpannerMatchers.isSpannerException; +import static com.google.common.testing.SerializableTester.reserialize; +import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; import com.google.cloud.ByteArray; +import com.google.cloud.Date; +import com.google.cloud.Timestamp; import com.google.cloud.spanner.spi.v1.SpannerRpc; import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.protobuf.ByteString; import com.google.protobuf.NullValue; @@ -403,26 +408,7 @@ public void multiResponseChunkingStructArray() { new Function, com.google.protobuf.Value>() { @Override public com.google.protobuf.Value apply(List input) { - // Value itself doesn't support serialization to proto, as struct isn't ever sent to the - // backend. Implement simple serialization ourselves. - com.google.protobuf.Value.Builder proto = com.google.protobuf.Value.newBuilder(); - for (Struct element : input) { - com.google.protobuf.Value.Builder elementProto = - proto.getListValueBuilder().addValuesBuilder(); - if (element == null) { - elementProto.setNullValue(NullValue.NULL_VALUE); - } else { - elementProto - .getListValueBuilder() - .addValuesBuilder() - .setStringValue(element.getString(0)); - elementProto - .getListValueBuilder() - .addValuesBuilder() - .setStringValue(Long.toString(element.getLong(1))); - } - } - return proto.build(); + return toProto(input); } }, new Function>() { @@ -433,6 +419,29 @@ public List apply(StructReader input) { }); } + private com.google.protobuf.Value toProto(List input) { + // Value itself doesn't support serialization to proto, as struct isn't ever sent to the + // backend. Implement simple serialization ourselves. + com.google.protobuf.Value.Builder proto = com.google.protobuf.Value.newBuilder(); + for (Struct element : input) { + com.google.protobuf.Value.Builder elementProto = + proto.getListValueBuilder().addValuesBuilder(); + if (element == null) { + elementProto.setNullValue(NullValue.NULL_VALUE); + } else { + elementProto + .getListValueBuilder() + .addValuesBuilder() + .setStringValue(element.getString(0)); + elementProto + .getListValueBuilder() + .addValuesBuilder() + .setStringValue(Long.toString(element.getLong(1))); + } + } + return proto.build(); + } + @Test public void profileResultInFinalResultSet() { Map statsMap = @@ -588,4 +597,82 @@ private static ResultSetMetadata makeMetadata(Type rowType) { com.google.spanner.v1.Type typeProto = rowType.toProto(); return ResultSetMetadata.newBuilder().setRowType(typeProto.getStructType()).build(); } + + @Test + public void serialization() throws Exception { + verifySerialization(Value.string("a"), + Value.string(null), + Value.bool(true), + Value.bool(null), + Value.int64(1), + Value.int64(null), + Value.float64(1.0), + Value.float64(null), + Value.bytes(ByteArray.fromBase64("abcd")), + Value.bytes(null), + Value.timestamp(Timestamp.ofTimeSecondsAndNanos(1, 2)), + Value.timestamp(null), + Value.date(Date.fromYearMonthDay(2017, 04, 17)), + Value.date(null), + Value.stringArray(ImmutableList.of("one", "two")), + Value.stringArray(null), + Value.boolArray(new boolean[]{true, false}), + Value.boolArray((boolean[]) null), + Value.int64Array(new long[]{1, 2, 3}), + Value.int64Array((long[]) null), + Value.timestampArray(ImmutableList.of(Timestamp.MAX_VALUE, Timestamp.MAX_VALUE)), + Value.timestampArray(null), + Value.dateArray(ImmutableList.of( + Date.fromYearMonthDay(2017, 4, 17), Date.fromYearMonthDay(2017, 5, 18))), + Value.dateArray(null) + ); + } + + @Test + public void nestedStructSerialization() throws Exception { + Type.StructField[] structFields = { + Type.StructField.of("a", Type.string()), + Type.StructField.of("b", Type.int64()) + }; + + Struct nestedStruct = s("1", 2L); + Value struct = Value.structArray(Arrays.asList(structFields), Arrays.asList(nestedStruct)); + verifySerialization(new Function() { + + @Override @Nullable public com.google.protobuf.Value apply(@Nullable Value input) { + return toProto(input.getStructArray()); + } + }, struct); + + } + + private void verifySerialization(Value... values) { + verifySerialization(new Function() { + + @Override + @Nullable + public com.google.protobuf.Value apply(@Nullable Value input) { + return input.toProto(); + } + }, values); + } + private void verifySerialization( Function + protoFn, Value... values) { + resultSet = new SpannerImpl.GrpcResultSet(stream, new NoOpListener(), QueryMode.NORMAL); + PartialResultSet.Builder builder = PartialResultSet.newBuilder(); + List types = new ArrayList<>(); + for (Value value : values) { + types.add(Type.StructField.of("f", value.getType())); + builder.addValues(protoFn.apply(value)); + } + consumer.onPartialResultSet( + builder.setMetadata(makeMetadata(Type.struct(types))) + .build()); + consumer.onCompleted(); + assertThat(resultSet.next()).isTrue(); + Struct row = resultSet.getCurrentRowAsStruct(); + Struct copy = reserialize(row); + assertThat(row).isEqualTo(copy); + } + } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StatementTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StatementTest.java index c1a96cbd52e5..904d96ba3005 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StatementTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/StatementTest.java @@ -16,8 +16,10 @@ package com.google.cloud.spanner; +import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; +import com.google.cloud.ByteArray; import com.google.common.collect.ImmutableMap; import com.google.common.testing.EqualsTester; import org.junit.Rule; @@ -38,6 +40,20 @@ public void basic() { assertThat(stmt.getSql()).isEqualTo(sql); assertThat(stmt.getParameters()).isEmpty(); assertThat(stmt.toString()).isEqualTo(sql); + reserializeAndAssert(stmt); + } + + @Test + public void serialization() throws Exception { + Statement stmt = Statement.newBuilder("SELECT * FROM table WHERE ") + .append("bool_field = @bool_field ").bind("bool_field").to(true) + .append("long_field = @long_field ").bind("long_field").to(1L) + .append("float_field = @float_field ").bind("float_field").to(1.) + .append("string_field = @string_field ").bind("string_field").to("abc") + .append("bytes_field = @bytes_field ").bind("bytes_field") + .to(ByteArray.fromBase64("abcd")) + .build(); + reserializeAndAssert(stmt); } @Test diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TimestampBoundTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TimestampBoundTest.java index 1244d6229936..51bc0276e900 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TimestampBoundTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TimestampBoundTest.java @@ -17,6 +17,7 @@ package com.google.cloud.spanner; import static com.google.cloud.spanner.TimestampBound.Mode; +import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; import com.google.cloud.Timestamp; @@ -37,6 +38,15 @@ public class TimestampBoundTest { private static final String TEST_TIME_ISO = "2015-10-12T15:14:54Z"; @Rule public ExpectedException expectedException = ExpectedException.none(); + @Test + public void serialization() throws Exception { + reserializeAndAssert(TimestampBound.strong()); + reserializeAndAssert(TimestampBound.ofExactStaleness(10, TimeUnit.NANOSECONDS)); + reserializeAndAssert(TimestampBound.ofMaxStaleness(100, TimeUnit.DAYS)); + reserializeAndAssert(TimestampBound.ofMinReadTimestamp(Timestamp.now())); + reserializeAndAssert(TimestampBound.ofReadTimestamp(Timestamp.now())); + } + @Test public void strong() { TimestampBound bound = TimestampBound.strong(); diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java index b3c7d7848ec5..2966ef80cff5 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ValueTest.java @@ -22,8 +22,13 @@ import com.google.cloud.ByteArray; import com.google.cloud.Date; import com.google.cloud.Timestamp; +import com.google.common.collect.ForwardingList; import com.google.common.collect.Lists; import com.google.common.testing.EqualsTester; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; @@ -713,28 +718,56 @@ public void serialization() { reserializeAndAssert(Value.bytes(newByteArray("abc"))); reserializeAndAssert(Value.bytes(null)); - reserializeAndAssert( - Value.boolArray(new boolean[] {false, true})); - reserializeAndAssert(Value.boolArray(Arrays.asList(true, false))); + reserializeAndAssert(Value.boolArray(new boolean[] { false, true })); + reserializeAndAssert(Value.boolArray(BrokenSerializationList.of(true, false))); reserializeAndAssert(Value.boolArray((Iterable) null)); - reserializeAndAssert( - Value.int64Array(Arrays.asList(1L, 2L))); - reserializeAndAssert( - Value.int64Array(new long[] {1L, 2L})); + reserializeAndAssert(Value.int64Array(BrokenSerializationList.of(1L, 2L))); + reserializeAndAssert(Value.int64Array(new long[] { 1L, 2L })); reserializeAndAssert(Value.int64Array((Iterable) null)); - reserializeAndAssert( - Value.float64Array(new double[] {.1, .2})); - reserializeAndAssert(Value.float64Array(Arrays.asList(.1, .2, .3))); + reserializeAndAssert(Value.float64Array(new double[] { .1, .2 })); + reserializeAndAssert(Value.float64Array(BrokenSerializationList.of(.1, .2, .3))); reserializeAndAssert(Value.float64Array((Iterable) null)); - reserializeAndAssert( - Value.stringArray(Arrays.asList("a", "b"))); + BrokenSerializationList of = BrokenSerializationList.of("a", "b"); + reserializeAndAssert(Value.stringArray(of)); reserializeAndAssert(Value.stringArray(null)); reserializeAndAssert( - Value.bytesArray(Arrays.asList(newByteArray("a"), newByteArray("b")))); + Value.bytesArray(BrokenSerializationList.of(newByteArray("a"), newByteArray("b")))); reserializeAndAssert(Value.bytesArray(null)); } + + @Test(expected = IllegalStateException.class) + public void verifyBrokenSerialization() { + reserializeAndAssert(BrokenSerializationList.of(1, 2, 3)); + } + + private static class BrokenSerializationList extends ForwardingList implements + Serializable { + private final List delegate; + + public static BrokenSerializationList of(T... values) { + return new BrokenSerializationList<>(Arrays.asList(values)); + } + + private BrokenSerializationList(List delegate) { + this.delegate = delegate; + } + + @Override protected List delegate() { + return delegate; + } + private void readObject(java.io.ObjectInputStream stream) + throws IOException, ClassNotFoundException { + throw new IllegalStateException("Serialization disabled"); + } + private void writeObject(java.io.ObjectOutputStream stream) + throws IOException { + throw new IllegalStateException("Serialization disabled"); + } + + + } }