From dd5d0232524db013d8d2d00284008b086d873fcf Mon Sep 17 00:00:00 2001 From: Cameron Porter Date: Mon, 23 Mar 2020 18:01:24 -0500 Subject: [PATCH] Allow type coercion when determining APi types When we read objects from the API, we want to ensure the type matches either the type class that the client is aware of, or whatever the complexType property on the object tells us. In some cases, the API returns a type for a property that is not the same or a subtype of the type defined by the API. The API should not do this, however the previous behavior was to throw an exception in these cases, which is quite annoying. Instead, we will now coerce the object we get into the type that the definition says. If properties are missing, they will remain unset, and any extra properties will be added to the unknown properties. --- .../api/json/GsonJsonMarshallerFactory.java | 14 +-- .../json/GsonJsonMarshallerFactoryTest.java | 95 ++++++++++++++++--- .../com/softlayer/api/service/TestEntity.java | 1 - .../com/softlayer/api/service/TestThing.java | 2 + 4 files changed, 87 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/softlayer/api/json/GsonJsonMarshallerFactory.java b/src/main/java/com/softlayer/api/json/GsonJsonMarshallerFactory.java index f77eb2b..e47d558 100644 --- a/src/main/java/com/softlayer/api/json/GsonJsonMarshallerFactory.java +++ b/src/main/java/com/softlayer/api/json/GsonJsonMarshallerFactory.java @@ -14,12 +14,8 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Base64; -import java.util.GregorianCalendar; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.Supplier; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -191,12 +187,12 @@ public Entity read(JsonReader in) throws IOException { // we're an adapter for. So if we have SoftLayer_Something and a newer release of the // API has a type extending it but we don't have a generated class for it, it will get // properly serialized to a SoftLayer_Something. + // If the API returns a type that isn't the same or a subtype of the type class, + // try as best we can to fit the data within the type class. Class clazz = typeClasses.get(apiTypeName); Entity result; - if (clazz == null) { + if (clazz == null || !typeClass.isAssignableFrom(clazz)) { result = readForThisType(in); - } else if (!typeClass.isAssignableFrom(clazz)) { - throw new RuntimeException("Expecting " + typeClass + " to be super type of " + clazz); } else { result = ((EntityTypeAdapter) gson.getAdapter(clazz)).readForThisType(in); } diff --git a/src/test/java/com/softlayer/api/json/GsonJsonMarshallerFactoryTest.java b/src/test/java/com/softlayer/api/json/GsonJsonMarshallerFactoryTest.java index 06ea291..5233db7 100644 --- a/src/test/java/com/softlayer/api/json/GsonJsonMarshallerFactoryTest.java +++ b/src/test/java/com/softlayer/api/json/GsonJsonMarshallerFactoryTest.java @@ -21,6 +21,7 @@ import com.google.gson.reflect.TypeToken; import com.softlayer.api.service.Entity; import com.softlayer.api.service.TestEntity; +import com.softlayer.api.service.TestThing; public class GsonJsonMarshallerFactoryTest { @@ -51,21 +52,22 @@ private String toJson(Object obj) throws Exception { public void testRead() throws Exception { Entity entity = fromJson(Entity.class, "{" - + "\"complexType\": \"SoftLayer_TestEntity\"," - + "\"bar\": \"some string\"," - + "\"foo\": \"another string\"," - + "\"baz\": null," - + "\"date\": \"1984-02-25T20:15:25-06:00\"," - + "\"notApiProperty\": \"bad value\"," - + "\"child\": {" - + " \"complexType\": \"SoftLayer_TestEntity\"," - + " \"bar\": \"child string\"" - + "}," - + "\"moreChildren\": [" - + " { \"complexType\": \"SoftLayer_TestEntity\", \"bar\": \"child 1\" }," - + " { \"complexType\": \"SoftLayer_TestEntity\", \"bar\": \"child 2\" }" - + "]" - + "}"); + + "\"complexType\": \"SoftLayer_TestEntity\"," + + "\"bar\": \"some string\"," + + "\"foo\": \"another string\"," + + "\"baz\": null," + + "\"date\": \"1984-02-25T20:15:25-06:00\"," + + "\"notApiProperty\": \"bad value\"," + + "\"child\": {" + + " \"complexType\": \"SoftLayer_TestEntity\"," + + " \"bar\": \"child string\"" + + "}," + + "\"moreChildren\": [" + + " { \"complexType\": \"SoftLayer_TestEntity\", \"bar\": \"child 1\" }," + + " { \"complexType\": \"SoftLayer_TestEntity\", \"bar\": \"child 2\" }" + + "]," + + "\"testThing\": {\"complexType\": \"SoftLayer_TestThing\", \"id\": 123}" + + "}"); assertEquals(TestEntity.class, entity.getClass()); TestEntity obj = (TestEntity) entity; assertEquals("some string", obj.getFoo()); @@ -85,6 +87,69 @@ public void testRead() throws Exception { assertEquals(2, obj.getMoreChildren().size()); assertEquals("child 1", obj.getMoreChildren().get(0).getFoo()); assertEquals("child 2", obj.getMoreChildren().get(1).getFoo()); + assertEquals(TestThing.class, obj.getTestThing().getClass()); + assertEquals(123, obj.getTestThing().getId().intValue()); + } + + @Test + public void testReadPropertyWithIncorrectComplexTypeCoercesTheType() throws Exception { + Entity entity = fromJson(Entity.class, + "{" + + "\"complexType\": \"SoftLayer_TestEntity\"," + + "\"testThing\": {\"complexType\": \"SoftLayer_TestEntity\", \"id\": 123, \"foo\": \"unknown!\"}" + + "}"); + assertEquals(TestEntity.class, entity.getClass()); + TestEntity obj = (TestEntity) entity; + assertEquals(0, obj.getUnknownProperties().size()); + assertEquals(TestThing.class, obj.getTestThing().getClass()); + assertEquals(123, obj.getTestThing().getId().intValue()); + assertEquals(1, obj.getTestThing().getUnknownProperties().size()); + assertEquals("unknown!", obj.getTestThing().getUnknownProperties().get("foo")); + } + + + @Test + public void testReadPropertyWithUnknownComplexTypeCoercesTheType() throws Exception { + Entity entity = fromJson(Entity.class, + "{" + + "\"complexType\": \"SoftLayer_TestEntity\"," + + "\"testThing\": {\"complexType\": \"WhoKnows\", \"id\": 123, \"foo\": \"unknown!\"}" + + "}"); + assertEquals(TestEntity.class, entity.getClass()); + TestEntity obj = (TestEntity) entity; + assertEquals(0, obj.getUnknownProperties().size()); + assertEquals(TestThing.class, obj.getTestThing().getClass()); + assertEquals(123, obj.getTestThing().getId().intValue()); + assertEquals(1, obj.getTestThing().getUnknownProperties().size()); + assertEquals("unknown!", obj.getTestThing().getUnknownProperties().get("foo")); + } + + @Test + public void testReadPropertyThrowsExceptionWithoutComplexType() { + + Exception e = assertThrows(RuntimeException.class, () -> { + Entity entity = fromJson(Entity.class, + "{" + + "\"complexType\": \"SoftLayer_TestEntity\"," + + "\"testThing\": {\"id\": 123}" + + "}"); + }); + + assertEquals("Expected 'complexType' as first property", e.getMessage()); + } + + @Test + public void testReadPropertyThrowsExceptioWithComplexTypeNotFirst() { + + Exception e = assertThrows(RuntimeException.class, () -> { + Entity entity = fromJson(Entity.class, + "{" + + "\"testThing\": {\"id\": 123}," + + "\"complexType\": \"SoftLayer_TestEntity\"" + + "}"); + }); + + assertEquals("Expected 'complexType' as first property", e.getMessage()); } @Test diff --git a/src/test/java/com/softlayer/api/service/TestEntity.java b/src/test/java/com/softlayer/api/service/TestEntity.java index ce85bf6..df3f7d8 100644 --- a/src/test/java/com/softlayer/api/service/TestEntity.java +++ b/src/test/java/com/softlayer/api/service/TestEntity.java @@ -11,7 +11,6 @@ import com.softlayer.api.annotation.ApiType; import com.softlayer.api.ApiClient; import com.softlayer.api.ResponseHandler; -import com.softlayer.api.ResultLimit; @ApiType("SoftLayer_TestEntity") public class TestEntity extends Entity { diff --git a/src/test/java/com/softlayer/api/service/TestThing.java b/src/test/java/com/softlayer/api/service/TestThing.java index 2eac363..548b87c 100644 --- a/src/test/java/com/softlayer/api/service/TestThing.java +++ b/src/test/java/com/softlayer/api/service/TestThing.java @@ -10,7 +10,9 @@ import com.softlayer.api.annotation.ApiMethod; import com.softlayer.api.annotation.ApiProperty; import com.softlayer.api.annotation.ApiService; +import com.softlayer.api.annotation.ApiType; +@ApiType("SoftLayer_TestThing") public class TestThing extends Entity { @ApiProperty(canBeNullOrNotSet = true)