Skip to content

Commit

Permalink
feat: GoogleCloudPlatform#2576 Added typeadapter to read and write In…
Browse files Browse the repository at this point in the history
…stant values.
  • Loading branch information
ablx committed May 3, 2024
1 parent dcec57a commit 06901d6
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@

import com.google.cloud.spring.data.spanner.core.convert.ConverterAwareMappingSpannerEntityProcessor;
import com.google.cloud.spring.data.spanner.core.convert.SpannerEntityProcessor;
import com.google.cloud.spring.data.spanner.core.mapping.typeadapter.InstantTypeAdapter;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import java.time.Instant;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
Expand Down Expand Up @@ -50,12 +53,11 @@ public class SpannerMappingContext

private Gson gson;

public SpannerMappingContext() {
}
public SpannerMappingContext() {}

public SpannerMappingContext(Gson gson) {
Assert.notNull(gson, "A non-null gson is required.");
this.gson = gson;
this.gson = addTypeAdapter(gson, new InstantTypeAdapter(), Instant.class);
}

@NonNull
Expand Down Expand Up @@ -96,7 +98,8 @@ protected <T> SpannerPersistentEntity<T> createPersistentEntity(
protected <T> SpannerPersistentEntityImpl<T> constructPersistentEntity(
TypeInformation<T> typeInformation) {
SpannerEntityProcessor processor;
if (this.applicationContext == null || !this.applicationContext.containsBean("spannerConverter")) {
if (this.applicationContext == null
|| !this.applicationContext.containsBean("spannerConverter")) {
processor = new ConverterAwareMappingSpannerEntityProcessor(this);
} else {
processor = this.applicationContext.getBean(SpannerEntityProcessor.class);
Expand Down Expand Up @@ -124,4 +127,9 @@ public SpannerPersistentEntity<?> getPersistentEntityOrFail(Class<?> entityClass
}
return entity;
}

private <T> Gson addTypeAdapter(Gson gson, TypeAdapter<T> typeAdapter, Class<T> type) {

return gson.newBuilder().registerTypeAdapter(type, typeAdapter).create();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.google.cloud.spring.data.spanner.core.mapping.typeadapter;

import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.time.Instant;

public class InstantTypeAdapter extends TypeAdapter<Instant> {
@Override
public void write(JsonWriter jsonWriter, Instant instant) throws IOException {
if (instant == null) {
jsonWriter.nullValue();
} else {
jsonWriter.value(instant.toString());
}
}

@Override
public Instant read(JsonReader jsonReader) throws IOException {
System.out.println(jsonReader.peek());
if (jsonReader.peek() == JsonToken.NULL) {
return null;
}
return Instant.parse(jsonReader.nextString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.google.cloud.spring.data.spanner.core.mapping.SpannerDataException;
import com.google.cloud.spring.data.spanner.core.mapping.SpannerMappingContext;
import com.google.gson.Gson;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
Expand All @@ -61,8 +62,7 @@ void setup() {
this.spannerReadConverter = new SpannerReadConverter();
SpannerMappingContext mappingContext = new SpannerMappingContext(new Gson());
this.spannerEntityReader =
new ConverterAwareMappingSpannerEntityReader(
mappingContext, this.spannerReadConverter);
new ConverterAwareMappingSpannerEntityReader(mappingContext, this.spannerReadConverter);
}

@Test
Expand Down Expand Up @@ -132,8 +132,8 @@ void readArraySingularMismatchTest() {
.build();

assertThatThrownBy(() -> this.spannerEntityReader.read(OuterTestEntity.class, rowStruct))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Column is not an ARRAY type: innerTestEntities");
.isInstanceOf(SpannerDataException.class)
.hasMessage("Column is not an ARRAY type: innerTestEntities");
}

@Test
Expand All @@ -148,19 +148,23 @@ void readSingularArrayMismatchTest() {
Type.struct(StructField.of("string_col", Type.string())), List.of(colStruct))
.build();

ConverterAwareMappingSpannerEntityReader testReader = new ConverterAwareMappingSpannerEntityReader(new SpannerMappingContext(), new SpannerReadConverter(
List.of(
new Converter<Struct, Integer>() {
@Nullable
@Override
public Integer convert(Struct source) {
return source.getString("string_col").length();
}
})));
ConverterAwareMappingSpannerEntityReader testReader =
new ConverterAwareMappingSpannerEntityReader(
new SpannerMappingContext(),
new SpannerReadConverter(
List.of(
new Converter<Struct, Integer>() {
@Nullable
@Override
public Integer convert(Struct source) {
return source.getString("string_col").length();
}
})));
assertThatThrownBy(() -> testReader.read(OuterTestEntityFlatFaulty.class, rowStruct))
.isInstanceOf(SpannerDataException.class)
.hasMessage("The value in column with name innerLengths could not be converted to the corresponding"
+ " property in the entity. The property's type is class java.lang.Integer.");
.isInstanceOf(SpannerDataException.class)
.hasMessage(
"The value in column with name innerLengths could not be converted to the corresponding"
+ " property in the entity. The property's type is class java.lang.Integer.");
}

@Test
Expand Down Expand Up @@ -218,8 +222,8 @@ void readNotFoundColumnTest() {
.build();

assertThatThrownBy(() -> this.spannerEntityReader.read(TestEntity.class, struct))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unable to read column from Cloud Spanner results: id4");
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unable to read column from Cloud Spanner results: id4");
}

@Test
Expand Down Expand Up @@ -255,11 +259,11 @@ void readUnconvertableValueTest() {
.to(Value.bytes(ByteArray.copyFrom("string1")))
.build();


assertThatThrownBy(() -> this.spannerEntityReader.read(TestEntity.class, struct))
.isInstanceOf(ConversionFailedException.class)
.hasMessage("Failed to convert from type [java.lang.String] to type "
+ "[java.lang.Double] for value [UNCONVERTABLE VALUE]")
.hasMessage(
"Failed to convert from type [java.lang.String] to type "
+ "[java.lang.Double] for value [UNCONVERTABLE VALUE]")
.hasStackTraceContaining(
"java.lang.NumberFormatException: For input string: \"UNCONVERTABLEVALUE\"");
}
Expand All @@ -270,8 +274,8 @@ void readUnmatachableTypesTest() {
Struct.newBuilder().set("fieldWithUnsupportedType").to(Value.string("key1")).build();

assertThatThrownBy(() -> this.spannerEntityReader.read(FaultyTestEntity.class, struct))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unable to read column from Cloud Spanner results: id");
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unable to read column from Cloud Spanner results: id");
}

@Test
Expand Down Expand Up @@ -329,8 +333,7 @@ void testPartialConstructor() {
void ensureConstructorArgsAreReadOnce() {
Struct row = mock(Struct.class);
when(row.getString("id")).thenReturn("1234");
when(row.getType())
.thenReturn(Type.struct(List.of(StructField.of("id", Type.string()))));
when(row.getType()).thenReturn(Type.struct(List.of(StructField.of("id", Type.string()))));
when(row.getColumnType("id")).thenReturn(Type.string());

TestEntities.SimpleConstructorTester result =
Expand All @@ -354,9 +357,10 @@ void testPartialConstructorWithNotEnoughArgs() {
.to(Value.float64(3.14))
.build();

assertThatThrownBy(() -> this.spannerEntityReader.read(TestEntities.PartialConstructor.class, struct))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Column not found: custom_col");
assertThatThrownBy(
() -> this.spannerEntityReader.read(TestEntities.PartialConstructor.class, struct))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Column not found: custom_col");
}

@Test
Expand All @@ -367,11 +371,16 @@ void zeroArgsListShouldNotThrowError() {
.set("zeroArgsListOfObjects")
.to(Value.stringArray(List.of("hello", "world")))
.build();
// Starting from Spring 3.0, Collection types without generics can be resolved to type with wildcard
// generics (i.e., "?"). For example, "zeroArgsListOfObjects" will be resolved to List<?>, rather
// Starting from Spring 3.0, Collection types without generics can be resolved to type with
// wildcard
// generics (i.e., "?"). For example, "zeroArgsListOfObjects" will be resolved to List<?>,
// rather
// than List.
assertThatNoException()
.isThrownBy(() -> this.spannerEntityReader.read(TestEntities.TestEntityWithListWithZeroTypeArgs.class, struct));
.isThrownBy(
() ->
this.spannerEntityReader.read(
TestEntities.TestEntityWithListWithZeroTypeArgs.class, struct));
}

@Test
Expand All @@ -397,6 +406,28 @@ void readJsonFieldTest() {
assertThat(result.params.p2).isEqualTo("5");
}

@Test
void readJsonInstantFieldTest() {
Struct row = mock(Struct.class);
when(row.getString("id")).thenReturn("1234");
when(row.getType())
.thenReturn(
Type.struct(
Arrays.asList(
Type.StructField.of("id", Type.string()),
Type.StructField.of("params", Type.json()))));
when(row.getColumnType("id")).thenReturn(Type.string());

when(row.getJson("params")).thenReturn("{\"instant\":\"1970-01-01T00:00:00Z\"}");

TestEntities.TestEntityInstantInJson result =
this.spannerEntityReader.read(TestEntities.TestEntityInstantInJson.class, row);

assertThat(result.id).isEqualTo("1234");

assertThat(result.params.instant).isEqualTo(Instant.ofEpochSecond(0));
}

@Test
void readArrayJsonFieldTest() {
Struct row = mock(Struct.class);
Expand All @@ -410,9 +441,12 @@ void readArrayJsonFieldTest() {
when(row.getColumnType("id")).thenReturn(Type.string());

when(row.getColumnType("paramsList")).thenReturn(Type.array(Type.json()));
when(row.getJsonList("paramsList")).thenReturn(
Arrays.asList("{\"p1\":\"address line\",\"p2\":\"5\"}",
"{\"p1\":\"address line 2\",\"p2\":\"6\"}", null));
when(row.getJsonList("paramsList"))
.thenReturn(
Arrays.asList(
"{\"p1\":\"address line\",\"p2\":\"5\"}",
"{\"p1\":\"address line 2\",\"p2\":\"6\"}",
null));

TestEntities.TestEntityJsonArray result =
this.spannerEntityReader.read(TestEntities.TestEntityJsonArray.class, row);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ void setup() {
this.writeConverter = new SpannerWriteConverter();
SpannerMappingContext spannerMappingContext = new SpannerMappingContext(new Gson());
this.spannerEntityWriter =
new ConverterAwareMappingSpannerEntityWriter(
spannerMappingContext, this.writeConverter);
new ConverterAwareMappingSpannerEntityWriter(spannerMappingContext, this.writeConverter);
}

@Test
Expand Down Expand Up @@ -367,6 +366,24 @@ void writeJsonTest() {
verify(valueBinder).to(Value.json("{\"p1\":\"some value\",\"p2\":\"some other value\"}"));
}

@Test
void writeJsonWithInstantTest() {
TestEntities.InstantParam parameters = new TestEntities.InstantParam(Instant.ofEpochSecond(0));
TestEntities.TestEntityInstantInJson testEntity =
new TestEntities.TestEntityInstantInJson("id1", parameters);

WriteBuilder writeBuilder = mock(WriteBuilder.class);
ValueBinder<WriteBuilder> valueBinder = mock(ValueBinder.class);

when(writeBuilder.set("id")).thenReturn(valueBinder);
when(writeBuilder.set("params")).thenReturn(valueBinder);

this.spannerEntityWriter.write(testEntity, writeBuilder::set);

verify(valueBinder).to(testEntity.id);
verify(valueBinder).to(Value.json("{\"instant\":\"1970-01-01T00:00:00Z\"}"));
}

@Test
void writeNullJsonTest() {
TestEntities.TestEntityJson testEntity = new TestEntities.TestEntityJson("id1", null);
Expand All @@ -386,7 +403,8 @@ void writeNullJsonTest() {
@Test
void writeJsonArrayTest() {
TestEntities.Params parameters = new TestEntities.Params("some value", "some other value");
TestEntities.TestEntityJsonArray testEntity = new TestEntities.TestEntityJsonArray("id1", Arrays.asList(parameters, parameters));
TestEntities.TestEntityJsonArray testEntity =
new TestEntities.TestEntityJsonArray("id1", Arrays.asList(parameters, parameters));

WriteBuilder writeBuilder = mock(WriteBuilder.class);
ValueBinder<WriteBuilder> valueBinder = mock(ValueBinder.class);
Expand All @@ -407,7 +425,8 @@ void writeJsonArrayTest() {
@Test
void writeNullEmptyJsonArrayTest() {
TestEntities.TestEntityJsonArray testNull = new TestEntities.TestEntityJsonArray("id1", null);
TestEntities.TestEntityJsonArray testEmpty = new TestEntities.TestEntityJsonArray("id2", new ArrayList<>());
TestEntities.TestEntityJsonArray testEmpty =
new TestEntities.TestEntityJsonArray("id2", new ArrayList<>());

WriteBuilder writeBuilder = mock(WriteBuilder.class);
ValueBinder<WriteBuilder> valueBinder = mock(ValueBinder.class);
Expand All @@ -432,8 +451,8 @@ void writeUnsupportedTypeIterableTest() {
WriteBuilder writeBuilder = Mutation.newInsertBuilder("faulty_test_table_2");

assertThatThrownBy(() -> this.spannerEntityWriter.write(ft, writeBuilder::set))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unsupported mapping for type: interface java.util.List");
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unsupported mapping for type: interface java.util.List");
}

@Test
Expand All @@ -444,18 +463,18 @@ void writeIncompatibleTypeTest() {
WriteBuilder writeBuilder = Mutation.newInsertBuilder("faulty_test_table");

assertThatThrownBy(() -> this.spannerEntityWriter.write(ft, writeBuilder::set))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unsupported mapping for type: "
+ "class com.google.cloud.spring.data.spanner.core.convert.TestEntities$TestEntity");

.isInstanceOf(SpannerDataException.class)
.hasMessage(
"Unsupported mapping for type: "
+ "class com.google.cloud.spring.data.spanner.core.convert.TestEntities$TestEntity");
}

@Test
void writingNullToKeyShouldThrowException() {

assertThatThrownBy(() -> this.spannerEntityWriter.convertToKey(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Key of an entity to be written cannot be null!");
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Key of an entity to be written cannot be null!");
}

@Test
Expand All @@ -471,9 +490,10 @@ void testUserSetUnconvertableColumnType() {
new UserSetUnconvertableColumnType();
WriteBuilder writeBuilder = Mutation.newInsertBuilder("faulty_test_table");

assertThatThrownBy(() -> this.spannerEntityWriter.write(userSetUnconvertableColumnType, writeBuilder::set))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unsupported mapping for type: boolean");
assertThatThrownBy(
() -> this.spannerEntityWriter.write(userSetUnconvertableColumnType, writeBuilder::set))
.isInstanceOf(SpannerDataException.class)
.hasMessage("Unsupported mapping for type: boolean");
}

@Test
Expand Down

0 comments on commit 06901d6

Please sign in to comment.