diff --git a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/implementation/JacksonDatabind212.java b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/implementation/JacksonDatabind212.java index bfcaee158f2aa..c9f84888898e4 100644 --- a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/implementation/JacksonDatabind212.java +++ b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/implementation/JacksonDatabind212.java @@ -4,13 +4,22 @@ package com.azure.core.serializer.json.jackson.implementation; import com.azure.core.util.logging.ClientLogger; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer; import com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy; import com.fasterxml.jackson.databind.introspect.AnnotatedClass; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.util.AccessPattern; /** * Utility methods for Jackson Databind types when it's known that the version is 2.12+. @@ -27,6 +36,36 @@ final class JacksonDatabind212 { * @return The updated {@link ObjectMapper}. */ static ObjectMapper mutateXmlCoercions(ObjectMapper mapper) { + // https://github.com/FasterXML/jackson-dataformat-xml/pull/585/files fixed array and collection elements + // with coercion to be handled by the coercion config below which is a backwards compatibility breaking + // change for us. Handle empty string items within an array or collection as empty string. + mapper.registerModule(new SimpleModule().setDeserializerModifier(new BeanDeserializerModifier() { + @Override + public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, + JsonDeserializer deserializer) { + if (String.class.isAssignableFrom(beanDesc.getBeanClass())) { + return new DelegatingDeserializer(deserializer) { + @Override + protected JsonDeserializer newDelegatingInstance(JsonDeserializer newDelegatee) { + return this; + } + + @Override + public AccessPattern getNullAccessPattern() { + return AccessPattern.DYNAMIC; + } + + @Override + public Object getNullValue(DeserializationContext ctxt) throws JsonMappingException { + return (ctxt.getParser().getParsingContext().inArray()) ? "" : super.getNullValue(ctxt); + } + }; + } else { + return deserializer; + } + } + })); + mapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); return mapper; } diff --git a/sdk/core/azure-core-version-tests/pom.xml b/sdk/core/azure-core-version-tests/pom.xml index 14a846605db5b..fe5ea1bc7951c 100644 --- a/sdk/core/azure-core-version-tests/pom.xml +++ b/sdk/core/azure-core-version-tests/pom.xml @@ -101,6 +101,11 @@ provided + + com.azure + azure-core-test + 1.19.0-beta.1 + io.projectreactor reactor-test diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerTests.java new file mode 100644 index 0000000000000..a7c36c00dd3c1 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerTests.java @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import com.azure.core.version.tests.models.Foo; +import com.azure.core.version.tests.models.FooChild; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; + +public class AdditionalPropertiesSerializerTests { + @Test + public void canSerializeAdditionalProperties() throws Exception { + Foo foo = new Foo(); + foo.bar("hello.world"); + foo.baz(new ArrayList<>()); + foo.baz().add("hello"); + foo.baz().add("hello.world"); + foo.qux(new HashMap<>()); + foo.qux().put("hello", "world"); + foo.qux().put("a.b", "c.d"); + foo.qux().put("bar.a", "ttyy"); + foo.qux().put("bar.b", "uuzz"); + foo.additionalProperties(new HashMap<>()); + foo.additionalProperties().put("bar", "baz"); + foo.additionalProperties().put("a.b", "c.d"); + foo.additionalProperties().put("properties.bar", "barbar"); + + String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON); + Assertions.assertEquals("{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized); + } + + @Test + public void canDeserializeAdditionalProperties() throws Exception { + String wireValue = "{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + Foo deserialized = new JacksonAdapter().deserialize(wireValue, Foo.class, SerializerEncoding.JSON); + Assertions.assertNotNull(deserialized.additionalProperties()); + Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar")); + Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b")); + Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar")); + } + + @Test + public void canSerializeAdditionalPropertiesThroughInheritance() throws Exception { + Foo foo = new FooChild(); + foo.bar("hello.world"); + foo.baz(new ArrayList<>()); + foo.baz().add("hello"); + foo.baz().add("hello.world"); + foo.qux(new HashMap<>()); + foo.qux().put("hello", "world"); + foo.qux().put("a.b", "c.d"); + foo.qux().put("bar.a", "ttyy"); + foo.qux().put("bar.b", "uuzz"); + foo.additionalProperties(new HashMap<>()); + foo.additionalProperties().put("bar", "baz"); + foo.additionalProperties().put("a.b", "c.d"); + foo.additionalProperties().put("properties.bar", "barbar"); + + String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON); + Assertions.assertEquals("{\"$type\":\"foochild\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized); + } + + @Test + public void canDeserializeAdditionalPropertiesThroughInheritance() throws Exception { + String wireValue = "{\"$type\":\"foochild\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + Foo deserialized = new JacksonAdapter().deserialize(wireValue, Foo.class, SerializerEncoding.JSON); + // Check additional properties are populated + Assertions.assertNotNull(deserialized.additionalProperties()); + Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar")); + Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b")); + Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar")); + Assertions.assertTrue(deserialized instanceof FooChild); + // Check typed properties are populated + Assertions.assertEquals("hello.world", deserialized.bar()); + Assertions.assertNotNull(deserialized.baz()); + Assertions.assertEquals(2, deserialized.baz().size()); + Assertions.assertTrue(deserialized.baz().contains("hello")); + Assertions.assertTrue(deserialized.baz().contains("hello.world")); + Assertions.assertNotNull(deserialized.qux()); + Assertions.assertEquals(4, deserialized.qux().size()); + Assertions.assertTrue(deserialized.qux().containsKey("hello")); + Assertions.assertTrue(deserialized.qux().containsKey("a.b")); + Assertions.assertTrue(deserialized.qux().containsKey("bar.a")); + Assertions.assertTrue(deserialized.qux().containsKey("bar.b")); + Assertions.assertEquals("world", deserialized.qux().get("hello")); + Assertions.assertEquals("c.d", deserialized.qux().get("a.b")); + Assertions.assertEquals("ttyy", deserialized.qux().get("bar.a")); + Assertions.assertEquals("uuzz", deserialized.qux().get("bar.b")); + } + + @Test + public void canSerializeAdditionalPropertiesWithNestedAdditionalProperties() throws Exception { + Foo foo = new Foo(); + foo.bar("hello.world"); + foo.baz(new ArrayList<>()); + foo.baz().add("hello"); + foo.baz().add("hello.world"); + foo.qux(new HashMap<>()); + foo.qux().put("hello", "world"); + foo.qux().put("a.b", "c.d"); + foo.qux().put("bar.a", "ttyy"); + foo.qux().put("bar.b", "uuzz"); + foo.additionalProperties(new HashMap<>()); + foo.additionalProperties().put("bar", "baz"); + foo.additionalProperties().put("a.b", "c.d"); + foo.additionalProperties().put("properties.bar", "barbar"); + Foo nestedFoo = new Foo(); + nestedFoo.bar("bye.world"); + nestedFoo.additionalProperties(new HashMap<>()); + nestedFoo.additionalProperties().put("name", "Sushi"); + foo.additionalProperties().put("foo", nestedFoo); + + String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON); + Assertions.assertEquals("{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"foo\":{\"properties\":{\"bar\":\"bye.world\"},\"name\":\"Sushi\"},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerWithJacksonAnnotationTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerWithJacksonAnnotationTests.java new file mode 100644 index 0000000000000..2383d7836a2c9 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/AdditionalPropertiesSerializerWithJacksonAnnotationTests.java @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import com.azure.core.version.tests.models.NewFoo; +import com.azure.core.version.tests.models.NewFooChild; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; + +public class AdditionalPropertiesSerializerWithJacksonAnnotationTests { + @Test + public void canSerializeAdditionalProperties() throws Exception { + NewFoo foo = new NewFoo(); + foo.bar("hello.world"); + foo.baz(new ArrayList<>()); + foo.baz().add("hello"); + foo.baz().add("hello.world"); + foo.qux(new HashMap<>()); + foo.qux().put("hello", "world"); + foo.qux().put("a.b", "c.d"); + foo.qux().put("bar.a", "ttyy"); + foo.qux().put("bar.b", "uuzz"); + foo.additionalProperties(new HashMap<>()); + foo.additionalProperties().put("bar", "baz"); + foo.additionalProperties().put("a.b", "c.d"); + foo.additionalProperties().put("properties.bar", "barbar"); + + String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON); + Assertions.assertEquals("{\"$type\":\"newfoo\",\"bar\":\"baz\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized); + } + + @Test + public void canDeserializeAdditionalProperties() throws Exception { + String wireValue = "{\"$type\":\"newfoo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + NewFoo deserialized = new JacksonAdapter().deserialize(wireValue, NewFoo.class, SerializerEncoding.JSON); + Assertions.assertNotNull(deserialized.additionalProperties()); + Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar")); + Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b")); + Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar")); + } + + @Test + public void canSerializeAdditionalPropertiesThroughInheritance() throws Exception { + NewFoo foo = new NewFooChild(); + foo.bar("hello.world"); + foo.baz(new ArrayList<>()); + foo.baz().add("hello"); + foo.baz().add("hello.world"); + foo.qux(new HashMap<>()); + foo.qux().put("hello", "world"); + foo.qux().put("a.b", "c.d"); + foo.qux().put("bar.a", "ttyy"); + foo.qux().put("bar.b", "uuzz"); + foo.additionalProperties(new HashMap<>()); + foo.additionalProperties().put("bar", "baz"); + foo.additionalProperties().put("a.b", "c.d"); + foo.additionalProperties().put("properties.bar", "barbar"); + + String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON); + Assertions.assertEquals("{\"$type\":\"newfoochild\",\"bar\":\"baz\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized); + } + + @Test + public void canDeserializeAdditionalPropertiesThroughInheritance() throws Exception { + String wireValue = "{\"$type\":\"newfoochild\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}"; + NewFoo deserialized = new JacksonAdapter().deserialize(wireValue, NewFoo.class, SerializerEncoding.JSON); + Assertions.assertNotNull(deserialized.additionalProperties()); + Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar")); + Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b")); + Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar")); + Assertions.assertTrue(deserialized instanceof NewFooChild); + } + + @Test + public void canSerializeAdditionalPropertiesWithNestedAdditionalProperties() throws Exception { + NewFoo foo = new NewFoo(); + foo.bar("hello.world"); + foo.baz(new ArrayList<>()); + foo.baz().add("hello"); + foo.baz().add("hello.world"); + foo.qux(new HashMap<>()); + foo.qux().put("hello", "world"); + foo.qux().put("a.b", "c.d"); + foo.qux().put("bar.a", "ttyy"); + foo.qux().put("bar.b", "uuzz"); + foo.additionalProperties(new HashMap<>()); + foo.additionalProperties().put("bar", "baz"); + foo.additionalProperties().put("a.b", "c.d"); + foo.additionalProperties().put("properties.bar", "barbar"); + NewFoo nestedNewFoo = new NewFoo(); + nestedNewFoo.bar("bye.world"); + nestedNewFoo.additionalProperties(new HashMap<>()); + nestedNewFoo.additionalProperties().put("name", "Sushi"); + foo.additionalProperties().put("foo", nestedNewFoo); + + String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON); + Assertions.assertEquals("{\"$type\":\"newfoo\",\"bar\":\"baz\",\"foo\":{\"name\":\"Sushi\",\"properties\":{\"bar\":\"bye.world\"}},\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized); + } + + @Test + public void canSerializeAdditionalPropertiesWithConflictProperty() throws Exception { + NewFoo foo = new NewFoo(); + foo.bar("hello.world"); + foo.baz(new ArrayList<>()); + foo.baz().add("hello"); + foo.baz().add("hello.world"); + foo.qux(new HashMap<>()); + foo.qux().put("hello", "world"); + foo.qux().put("a.b", "c.d"); + foo.qux().put("bar.a", "ttyy"); + foo.qux().put("bar.b", "uuzz"); + foo.additionalProperties(new HashMap<>()); + foo.additionalProperties().put("bar", "baz"); + foo.additionalProperties().put("a.b", "c.d"); + foo.additionalProperties().put("properties.bar", "barbar"); + foo.additionalPropertiesProperty(new HashMap<>()); + foo.additionalPropertiesProperty().put("age", 73); + + String serialized = new JacksonAdapter().serialize(foo, SerializerEncoding.JSON); + Assertions.assertEquals("{\"$type\":\"newfoo\",\"additionalProperties\":{\"age\":73},\"bar\":\"baz\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"a.b\":\"c.d\",\"properties.bar\":\"barbar\"}", serialized); + } + + @Test + public void canDeserializeAdditionalPropertiesWithConflictProperty() throws Exception { + String wireValue = "{\"$type\":\"newfoo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}},\"bar\":\"baz\",\"a.b\":\"c.d\",\"properties.bar\":\"barbar\",\"additionalProperties\":{\"age\":73}}"; + NewFoo deserialized = new JacksonAdapter().deserialize(wireValue, NewFoo.class, SerializerEncoding.JSON); + Assertions.assertNotNull(deserialized.additionalProperties()); + Assertions.assertEquals("baz", deserialized.additionalProperties().get("bar")); + Assertions.assertEquals("c.d", deserialized.additionalProperties().get("a.b")); + Assertions.assertEquals("barbar", deserialized.additionalProperties().get("properties.bar")); + Assertions.assertEquals(1, deserialized.additionalPropertiesProperty().size()); + Assertions.assertEquals(73, deserialized.additionalPropertiesProperty().get("age")); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/BinaryDataSerializationTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/BinaryDataSerializationTests.java new file mode 100644 index 0000000000000..9152bd81b89fc --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/BinaryDataSerializationTests.java @@ -0,0 +1,373 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.util.BinaryData; +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class BinaryDataSerializationTests { + private static final SerializerAdapter ADAPTER = JacksonAdapter.createDefaultSerializerAdapter(); + + private static final byte[] HELLO_BYTES = "hello".getBytes(StandardCharsets.UTF_8); + private static final String BASE64_HELLO_BYTES = Base64.getEncoder().encodeToString(HELLO_BYTES); + private static final SimplePojo SIMPLE_POJO = new SimplePojo() + .setString("hello") + .setNumber(3.14) + .setBool(true); + + private static final BinaryData FROM_BYTES = BinaryData.fromBytes(HELLO_BYTES); + private static final Supplier FROM_STREAM + = () -> BinaryData.fromStream(new ByteArrayInputStream(HELLO_BYTES), (long) HELLO_BYTES.length); + private static final BinaryData FROM_STRING = BinaryData.fromString("hello"); + private static final BinaryData FROM_OBJECT = BinaryData.fromObject(SIMPLE_POJO); + private static final BinaryData FROM_STRING_BASE64 = BinaryData.fromString(BASE64_HELLO_BYTES); + + private static final String EMPTY_OBJECT_JSON = "{}"; + + private static final String SIMPLE_PROPERTY_BYTES_JSON = "{\"simpleProperty\":\"" + BASE64_HELLO_BYTES + "\"}"; + private static final String SIMPLE_PROPERTY_STREAM_JSON = "{\"simpleProperty\":\"" + BASE64_HELLO_BYTES + "\"}"; + private static final String SIMPLE_PROPERTY_STRING_JSON = "{\"simpleProperty\":\"hello\"}"; + private static final String SIMPLE_PROPERTY_OBJECT_JSON + = "{\"simpleProperty\":{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}}"; + + private static final SimpleProperty SIMPLE_PROPERTY_NULL = new SimpleProperty(); + private static final SimpleProperty SIMPLE_PROPERTY_FROM_BYTES = new SimpleProperty() + .setSimpleProperty(FROM_BYTES); + private static final SimpleProperty SIMPLE_PROPERTY_FROM_STREAM = new SimpleProperty() + .setSimpleProperty(FROM_STREAM.get()); + private static final SimpleProperty SIMPLE_PROPERTY_FROM_STRING = new SimpleProperty() + .setSimpleProperty(FROM_STRING); + private static final SimpleProperty SIMPLE_PROPERTY_FROM_OBJECT = new SimpleProperty() + .setSimpleProperty(FROM_OBJECT); + private static final SimpleProperty SIMPLE_PROPERTY_BASE64 = new SimpleProperty() + .setSimpleProperty(FROM_STRING_BASE64); + + private static final String LIST_PROPERTY_BYTES_JSON = "{\"listProperty\":[\"" + BASE64_HELLO_BYTES + "\"]}"; + private static final String LIST_PROPERTY_STREAM_JSON = "{\"listProperty\":[\"" + BASE64_HELLO_BYTES + "\"]}"; + private static final String LIST_PROPERTY_STRING_JSON = "{\"listProperty\":[\"hello\"]}"; + private static final String LIST_PROPERTY_OBJECT_JSON + = "{\"listProperty\":[{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}]}"; + private static final String LIST_PROPERTY_MULTI_JSON + = "{\"listProperty\":[\"" + BASE64_HELLO_BYTES + "\",\"" + BASE64_HELLO_BYTES + "\",\"hello\"," + + "{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}]}"; + + private static final ListProperty LIST_PROPERTY_NULL = new ListProperty(); + private static final ListProperty LIST_PROPERTY_FROM_BYTES = new ListProperty() + .setListProperty(singletonList(FROM_BYTES)); + private static final ListProperty LIST_PROPERTY_FROM_STREAM = new ListProperty() + .setListProperty(singletonList(FROM_STREAM.get())); + private static final ListProperty LIST_PROPERTY_FROM_STRING = new ListProperty() + .setListProperty(singletonList(FROM_STRING)); + private static final ListProperty LIST_PROPERTY_FROM_OBJECT = new ListProperty() + .setListProperty(singletonList(FROM_OBJECT)); + private static final ListProperty LIST_PROPERTY_MULTIPLE = new ListProperty() + .setListProperty(Arrays.asList(FROM_BYTES, FROM_STREAM.get(), FROM_STRING, FROM_OBJECT)); + private static final ListProperty LIST_PROPERTY_BASE64 = new ListProperty() + .setListProperty(singletonList(FROM_STRING_BASE64)); + private static final ListProperty LIST_PROPERTY_BASE64_MULTIPLE = new ListProperty() + .setListProperty(Arrays.asList(FROM_STRING_BASE64, FROM_STRING_BASE64, FROM_STRING, FROM_OBJECT)); + + private static final String MAP_PROPERTY_BYTES_JSON = "{\"mapProperty\":{\"key\":\"" + BASE64_HELLO_BYTES + "\"}}"; + private static final String MAP_PROPERTY_STREAM_JSON = "{\"mapProperty\":{\"key\":\"" + BASE64_HELLO_BYTES + "\"}}"; + private static final String MAP_PROPERTY_STRING_JSON = "{\"mapProperty\":{\"key\":\"hello\"}}"; + private static final String MAP_PROPERTY_OBJECT_JSON + = "{\"mapProperty\":{\"key\":{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}}}"; + private static final String MAP_PROPERTY_MULTI_JSON = "{\"mapProperty\":{" + + "\"fromBytes\":\"" + BASE64_HELLO_BYTES + "\"," + + "\"fromStream\":\"" + BASE64_HELLO_BYTES + "\"," + + "\"fromString\":\"hello\"," + + "\"fromObject\":{\"string\":\"hello\",\"number\":3.14,\"boolean\":true}}}"; + + private static final MapProperty MAP_PROPERTY_NULL = new MapProperty(); + private static final MapProperty MAP_PROPERTY_FROM_BYTES = new MapProperty() + .setMapProperty(singletonMap("key", FROM_BYTES)); + private static final MapProperty MAP_PROPERTY_FROM_STREAM = new MapProperty() + .setMapProperty(singletonMap("key", FROM_STREAM.get())); + private static final MapProperty MAP_PROPERTY_FROM_STRING = new MapProperty() + .setMapProperty(singletonMap("key", FROM_STRING)); + private static final MapProperty MAP_PROPERTY_FROM_OBJECT = new MapProperty() + .setMapProperty(singletonMap("key", FROM_OBJECT)); + private static final MapProperty MAP_PROPERTY_MULTIPLE = new MapProperty() + .setMapProperty(createMapPropertyMultiMap(false)); + private static final MapProperty MAP_PROPERTY_BASE64 = new MapProperty() + .setMapProperty(singletonMap("key", FROM_STRING_BASE64)); + private static final MapProperty MAP_PROPERTY_BASE64_MULTIPLE = new MapProperty() + .setMapProperty(createMapPropertyMultiMap(true)); + + private static Map createMapPropertyMultiMap(boolean base64) { + Map map = new LinkedHashMap<>(); + map.put("fromBytes", base64 ? FROM_STRING_BASE64 : FROM_BYTES); + map.put("fromStream", base64 ? FROM_STRING_BASE64 : FROM_STREAM.get()); + map.put("fromString", FROM_STRING); + map.put("fromObject", FROM_OBJECT); + + return map; + } + + @ParameterizedTest + @MethodSource("binaryDataSerializationSupplier") + public void binaryDataSerialization(Object serializable, String expected) throws IOException { + String actual = ADAPTER.serialize(serializable, SerializerEncoding.JSON); + + assertEquals(expected, actual); + } + + private static Stream binaryDataSerializationSupplier() { + return Stream.of( + Arguments.of(SIMPLE_PROPERTY_NULL, EMPTY_OBJECT_JSON), + Arguments.of(SIMPLE_PROPERTY_FROM_BYTES, SIMPLE_PROPERTY_BYTES_JSON), + Arguments.of(SIMPLE_PROPERTY_FROM_STREAM, SIMPLE_PROPERTY_STREAM_JSON), + Arguments.of(SIMPLE_PROPERTY_FROM_STRING, SIMPLE_PROPERTY_STRING_JSON), + Arguments.of(SIMPLE_PROPERTY_FROM_OBJECT, SIMPLE_PROPERTY_OBJECT_JSON), + + Arguments.of(LIST_PROPERTY_NULL, EMPTY_OBJECT_JSON), + Arguments.of(LIST_PROPERTY_FROM_BYTES, LIST_PROPERTY_BYTES_JSON), + Arguments.of(LIST_PROPERTY_FROM_STREAM, LIST_PROPERTY_STREAM_JSON), + Arguments.of(LIST_PROPERTY_FROM_STRING, LIST_PROPERTY_STRING_JSON), + Arguments.of(LIST_PROPERTY_FROM_OBJECT, LIST_PROPERTY_OBJECT_JSON), + Arguments.of(LIST_PROPERTY_MULTIPLE, LIST_PROPERTY_MULTI_JSON), + + Arguments.of(MAP_PROPERTY_NULL, EMPTY_OBJECT_JSON), + Arguments.of(MAP_PROPERTY_FROM_BYTES, MAP_PROPERTY_BYTES_JSON), + Arguments.of(MAP_PROPERTY_FROM_STREAM, MAP_PROPERTY_STREAM_JSON), + Arguments.of(MAP_PROPERTY_FROM_STRING, MAP_PROPERTY_STRING_JSON), + Arguments.of(MAP_PROPERTY_FROM_OBJECT, MAP_PROPERTY_OBJECT_JSON), + Arguments.of(MAP_PROPERTY_MULTIPLE, MAP_PROPERTY_MULTI_JSON) + ); + } + + @ParameterizedTest + @MethodSource("binaryDataDeserializationSupplier") + @Execution(ExecutionMode.SAME_THREAD) + public void binaryDataDeserialization(Object expected, String json, Class type) throws IOException { + Object actual = ADAPTER.deserialize(json, type, SerializerEncoding.JSON); + + assertEquals(expected, actual); + } + + private static Stream binaryDataDeserializationSupplier() { + return Stream.of( + Arguments.of(SIMPLE_PROPERTY_NULL, EMPTY_OBJECT_JSON, SimpleProperty.class), + Arguments.of(SIMPLE_PROPERTY_BASE64, SIMPLE_PROPERTY_BYTES_JSON, SimpleProperty.class), + Arguments.of(SIMPLE_PROPERTY_BASE64, SIMPLE_PROPERTY_STREAM_JSON, SimpleProperty.class), + Arguments.of(SIMPLE_PROPERTY_FROM_STRING, SIMPLE_PROPERTY_STRING_JSON, SimpleProperty.class), + Arguments.of(SIMPLE_PROPERTY_FROM_OBJECT, SIMPLE_PROPERTY_OBJECT_JSON, SimpleProperty.class), + + Arguments.of(LIST_PROPERTY_NULL, EMPTY_OBJECT_JSON, ListProperty.class), + Arguments.of(LIST_PROPERTY_BASE64, LIST_PROPERTY_BYTES_JSON, ListProperty.class), + Arguments.of(LIST_PROPERTY_BASE64, LIST_PROPERTY_STREAM_JSON, ListProperty.class), + Arguments.of(LIST_PROPERTY_FROM_STRING, LIST_PROPERTY_STRING_JSON, ListProperty.class), + Arguments.of(LIST_PROPERTY_FROM_OBJECT, LIST_PROPERTY_OBJECT_JSON, ListProperty.class), + Arguments.of(LIST_PROPERTY_BASE64_MULTIPLE, LIST_PROPERTY_MULTI_JSON, ListProperty.class), + + Arguments.of(MAP_PROPERTY_NULL, EMPTY_OBJECT_JSON, MapProperty.class), + Arguments.of(MAP_PROPERTY_BASE64, MAP_PROPERTY_BYTES_JSON, MapProperty.class), + Arguments.of(MAP_PROPERTY_BASE64, MAP_PROPERTY_STREAM_JSON, MapProperty.class), + Arguments.of(MAP_PROPERTY_FROM_STRING, MAP_PROPERTY_STRING_JSON, MapProperty.class), + Arguments.of(MAP_PROPERTY_FROM_OBJECT, MAP_PROPERTY_OBJECT_JSON, MapProperty.class), + Arguments.of(MAP_PROPERTY_BASE64_MULTIPLE, MAP_PROPERTY_MULTI_JSON, MapProperty.class) + ); + } + + public static final class SimpleProperty { + @JsonProperty("simpleProperty") + private BinaryData simpleProperty; + + public BinaryData getSimpleProperty() { + return simpleProperty; + } + + public SimpleProperty setSimpleProperty(BinaryData simpleProperty) { + this.simpleProperty = simpleProperty; + return this; + } + + @Override + public int hashCode() { + return binaryDataHash(simpleProperty); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SimpleProperty)) { + return false; + } + + SimpleProperty other = (SimpleProperty) obj; + return binaryDataEquals(simpleProperty, other.simpleProperty); + } + } + + public static final class ListProperty { + @JsonProperty("listProperty") + private List listProperty; + + public List getListProperty() { + return listProperty; + } + + public ListProperty setListProperty(List listProperty) { + this.listProperty = listProperty; + return this; + } + + @Override + public int hashCode() { + return Objects.hash(listProperty); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ListProperty)) { + return false; + } + + ListProperty other = (ListProperty) obj; + + if (listProperty == null && other.listProperty == null) { + return true; + } else if (listProperty != null && other.listProperty == null) { + return false; + } else if (listProperty == null && other.listProperty != null) { + return false; + } + + if (listProperty.size() != other.listProperty.size()) { + return false; + } + + int size = listProperty.size(); + for (int i = 0; i < size; i++) { + if (!binaryDataEquals(listProperty.get(i), other.listProperty.get(i))) { + return false; + } + } + + return true; + } + } + + public static final class MapProperty { + @JsonProperty("mapProperty") + private Map mapProperty; + + public Map getMapProperty() { + return mapProperty; + } + + public MapProperty setMapProperty(Map mapProperty) { + this.mapProperty = mapProperty; + return this; + } + + @Override + public int hashCode() { + return Objects.hash(mapProperty); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MapProperty)) { + return false; + } + + MapProperty other = (MapProperty) obj; + + if (mapProperty == null && other.mapProperty == null) { + return true; + } else if (mapProperty != null && other.mapProperty == null) { + return false; + } else if (mapProperty == null && other.mapProperty != null) { + return false; + } + + if (mapProperty.size() != other.mapProperty.size()) { + return false; + } + + for (Map.Entry entry : mapProperty.entrySet()) { + if (!other.mapProperty.containsKey(entry.getKey())) { + return false; + } + + if (!binaryDataEquals(entry.getValue(), other.mapProperty.get(entry.getKey()))) { + return false; + } + } + + return true; + } + } + + public static final class SimplePojo { + @JsonProperty("string") + private String string; + + @JsonProperty("number") + private double number; + + @JsonProperty("boolean") + private boolean bool; + + public String getString() { + return string; + } + + public SimplePojo setString(String string) { + this.string = string; + return this; + } + + public double getNumber() { + return number; + } + + public SimplePojo setNumber(double number) { + this.number = number; + return this; + } + + public boolean isBool() { + return bool; + } + + public SimplePojo setBool(boolean bool) { + this.bool = bool; + return this; + } + } + + private static int binaryDataHash(BinaryData data) { + return Objects.hash(data == null ? null : data.toString()); + } + + private static boolean binaryDataEquals(BinaryData data1, BinaryData data2) { + return (data1 == null && data2 == null) + || (data1 != null && data2 != null && Objects.equals(data1.toString(), data2.toString())); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/CustomSerializerTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/CustomSerializerTests.java new file mode 100644 index 0000000000000..3e04758afa25d --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/CustomSerializerTests.java @@ -0,0 +1,376 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.models.GeoBoundingBox; +import com.azure.core.models.GeoCollection; +import com.azure.core.models.GeoLineString; +import com.azure.core.models.GeoLineStringCollection; +import com.azure.core.models.GeoLinearRing; +import com.azure.core.models.GeoObject; +import com.azure.core.models.GeoObjectType; +import com.azure.core.models.GeoPoint; +import com.azure.core.models.GeoPointCollection; +import com.azure.core.models.GeoPolygon; +import com.azure.core.models.GeoPolygonCollection; +import com.azure.core.models.GeoPosition; +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests for {@code DateTimeDeserializer}. + */ +public class CustomSerializerTests { + private static final SerializerAdapter ADAPTER = JacksonAdapter.createDefaultSerializerAdapter(); + + @ParameterizedTest + @MethodSource("offsetDateTimeDeserializationSupplier") + public void deserializeOffsetDateTime(String dateTimeJson, OffsetDateTime expected) throws IOException { + assertEquals(expected, ADAPTER.deserialize(dateTimeJson, OffsetDateTime.class, SerializerEncoding.JSON)); + } + + private static Stream offsetDateTimeDeserializationSupplier() { + return offsetDateTimeSupplier(true); + } + + @ParameterizedTest + @MethodSource("offsetDateTimeSerializationSupplier") + public void serializeOffsetDateTime(String expected, OffsetDateTime dateTime) throws IOException { + assertEquals(expected, ADAPTER.serialize(dateTime, SerializerEncoding.JSON)); + } + + private static Stream offsetDateTimeSerializationSupplier() { + return offsetDateTimeSupplier(false); + } + + private static Stream offsetDateTimeSupplier(boolean deserialization) { + OffsetDateTime minValue = OffsetDateTime.of(1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + OffsetDateTime unixEpoch = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + + OffsetDateTime nonUtcTimeZone = OffsetDateTime.of(2020, 1, 1, 0, 0, 0, 0, ZoneOffset.ofHours(-7)); + + if (deserialization) { + return Stream.of( + Arguments.of("\"0001-01-01T00:00:00\"", minValue), + Arguments.of(String.valueOf(minValue.toEpochSecond()), minValue), + Arguments.of("\"0001-01-01T00:00:00Z\"", minValue), + Arguments.of("\"1970-01-01T00:00:00\"", unixEpoch), + Arguments.of("\"1970-01-01T00:00:00Z\"", unixEpoch), + Arguments.of("\"2020-01-01T00:00:00-07:00\"", nonUtcTimeZone) + ); + } else { + return Stream.of( + Arguments.of("\"0001-01-01T00:00:00Z\"", minValue), + Arguments.of("\"0001-01-01T00:00:00Z\"", minValue), + Arguments.of("\"1970-01-01T00:00:00Z\"", unixEpoch), + Arguments.of("\"1970-01-01T00:00:00Z\"", unixEpoch), + Arguments.of("\"2020-01-01T07:00:00Z\"", nonUtcTimeZone) + ); + } + } + + @ParameterizedTest + @MethodSource("toStringTestSupplier") + public void toStringTest(Duration duration, String expected) throws IOException { + assertEquals(expected, ADAPTER.serialize(duration, SerializerEncoding.JSON)); + } + + private static Stream toStringTestSupplier() { + return Stream.of( + Arguments.of(Duration.ofMillis(0), "\"PT0S\""), + Arguments.of(Duration.ofMillis(1), "\"PT0.001S\""), + Arguments.of(Duration.ofMillis(9), "\"PT0.009S\""), + Arguments.of(Duration.ofMillis(10), "\"PT0.01S\""), + Arguments.of(Duration.ofMillis(11), "\"PT0.011S\""), + Arguments.of(Duration.ofMillis(99), "\"PT0.099S\""), + Arguments.of(Duration.ofMillis(100), "\"PT0.1S\""), + Arguments.of(Duration.ofMillis(101), "\"PT0.101S\""), + Arguments.of(Duration.ofMillis(999), "\"PT0.999S\""), + Arguments.of(Duration.ofMillis(1000), "\"PT1S\""), + Arguments.of(Duration.ofSeconds(1), "\"PT1S\""), + Arguments.of(Duration.ofSeconds(9), "\"PT9S\""), + Arguments.of(Duration.ofSeconds(10), "\"PT10S\""), + Arguments.of(Duration.ofSeconds(11), "\"PT11S\""), + Arguments.of(Duration.ofSeconds(59), "\"PT59S\""), + Arguments.of(Duration.ofSeconds(60), "\"PT1M\""), + Arguments.of(Duration.ofSeconds(61), "\"PT1M1S\""), + Arguments.of(Duration.ofMinutes(1), "\"PT1M\""), + Arguments.of(Duration.ofMinutes(9), "\"PT9M\""), + Arguments.of(Duration.ofMinutes(10), "\"PT10M\""), + Arguments.of(Duration.ofMinutes(11), "\"PT11M\""), + Arguments.of(Duration.ofMinutes(59), "\"PT59M\""), + Arguments.of(Duration.ofMinutes(60), "\"PT1H\""), + Arguments.of(Duration.ofMinutes(61), "\"PT1H1M\""), + Arguments.of(Duration.ofHours(1), "\"PT1H\""), + Arguments.of(Duration.ofHours(9), "\"PT9H\""), + Arguments.of(Duration.ofHours(10), "\"PT10H\""), + Arguments.of(Duration.ofHours(11), "\"PT11H\""), + Arguments.of(Duration.ofHours(23), "\"PT23H\""), + Arguments.of(Duration.ofHours(24), "\"P1D\""), + Arguments.of(Duration.ofHours(25), "\"P1DT1H\""), + Arguments.of(Duration.ofDays(1), "\"P1D\""), + Arguments.of(Duration.ofDays(9), "\"P9D\""), + Arguments.of(Duration.ofDays(10), "\"P10D\""), + Arguments.of(Duration.ofDays(11), "\"P11D\""), + Arguments.of(Duration.ofDays(99), "\"P99D\""), + Arguments.of(Duration.ofDays(100), "\"P100D\""), + Arguments.of(Duration.ofDays(101), "\"P101D\"") + ); + } + + @ParameterizedTest + @MethodSource("invalidGeoJsonDeserializationSupplier") + public void invalidGeoJsonDeserializationThrowsIllegalStateException(String invalidGeoJson) { + Assertions.assertThrows(IllegalStateException.class, + () -> ADAPTER.deserialize(invalidGeoJson, GeoObject.class, SerializerEncoding.JSON)); + } + + private static Stream invalidGeoJsonDeserializationSupplier() { + return Stream.of( + "{\"coordinates\":[0,0]}", // Missing type + "{\"type\":\"Point\"}", // Missing coordinates + "{\"type\":\"Custom\",\"coordinates\":[0,0]}", // Invalid/unknown type + "{\"type\":\"Point\",\"coordinates\":[1]}", // Invalid coordinates count + "{\"type\":\"Point\",\"coordinates\":[4,4,4,4]}", // Invalid coordinates count + "{\"type\":\"Point\",\"coordinates\":[0,0],\"bbox\":[2,2]}", // Invalid bounding box + "{\"type\":\"Point\",\"coordinates\":[0,0],\"bbox\":[8,8,8,8,8,8,8,8]}", // Invalid bounding box + "{\"type\":\"GeometryCollection\"}" // Collection without geometries + ); + } + + @ParameterizedTest + @MethodSource("geoJsonDeserializationSupplier") + public void geoJsonDeserialization(String json, Class type, T expectedGeo) throws IOException { + Assertions.assertEquals(expectedGeo, ADAPTER.deserialize(json, type, SerializerEncoding.JSON)); + } + + private static Stream geoJsonDeserializationSupplier() { + GeoBoundingBox boundingBox = new GeoBoundingBox(0, 0, 1, 1, 0D, 1D); + Map simpleProperties = Collections.singletonMap("key", "value"); + Map arrayProperties = Collections.singletonMap("text", Arrays.asList("hello", "world")); + + Map crs = new HashMap<>(); + crs.put("type", "name"); + crs.put("properties", Collections.singletonMap("name", "EPSG:432")); + Map objectProperties = Collections.singletonMap("crs", crs); + + BiFunction, GeoPoint> pointSupplier = + (box, properties) -> new GeoPoint(new GeoPosition(0, 0, 0D), box, properties); + + List positions = Arrays.asList(new GeoPosition(0, 0, 1D), + new GeoPosition(1, 1, 1D)); + BiFunction, GeoLineString> lineSupplier = + (box, properties) -> new GeoLineString(positions, box, properties); + + List rings = Collections.singletonList(new GeoLinearRing(Arrays.asList( + new GeoPosition(0, 0, 1D), new GeoPosition(0, 1, 1D), + new GeoPosition(1, 1, 1D), new GeoPosition(0, 0, 1D) + ))); + BiFunction, GeoPolygon> polygonSupplier = + (box, properties) -> new GeoPolygon(rings, box, properties); + + BiFunction, GeoPointCollection> multiPointSupplier = + (box, properties) -> new GeoPointCollection(Arrays.asList(pointSupplier.apply(null, null), + pointSupplier.apply(null, null)), box, properties); + + BiFunction, GeoLineStringCollection> multiLineSupplier = + (box, properties) -> new GeoLineStringCollection(Arrays.asList(lineSupplier.apply(null, null), + lineSupplier.apply(null, null)), box, properties); + + BiFunction, GeoPolygonCollection> multiPolygonSuppluer = + (box, properties) -> new GeoPolygonCollection(Arrays.asList(polygonSupplier.apply(null, null), + polygonSupplier.apply(null, null)), box, properties); + + BiFunction, GeoCollection> collectionSupplier = + (box, properties) -> new GeoCollection(Arrays.asList(pointSupplier.apply(null, null), + multiLineSupplier.apply(box, properties), polygonSupplier.apply(box, properties)), box, properties); + + return Stream.of( + // GeoPoint + Arguments.of(deserializerArgumentSupplier(null, null, pointSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, pointSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, pointSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, pointSupplier)), + + // GeoLine + Arguments.of(deserializerArgumentSupplier(null, null, lineSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, lineSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, lineSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, lineSupplier)), + + // GeoPolygon + Arguments.of(deserializerArgumentSupplier(null, null, polygonSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, polygonSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, polygonSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, polygonSupplier)), + + // GeoPointCollection + Arguments.of(deserializerArgumentSupplier(null, null, multiPointSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, multiPointSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, multiPointSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, multiPointSupplier)), + + // GeoLineCollection + Arguments.of(deserializerArgumentSupplier(null, null, multiLineSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, multiLineSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, multiLineSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, multiLineSupplier)), + + // GeoPolygonCollection + Arguments.of(deserializerArgumentSupplier(null, null, multiPolygonSuppluer)), + Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, multiPolygonSuppluer)), + Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, multiPolygonSuppluer)), + Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, multiPolygonSuppluer)), + + // GeoCollection + Arguments.of(deserializerArgumentSupplier(null, null, collectionSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, simpleProperties, collectionSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, arrayProperties, collectionSupplier)), + Arguments.of(deserializerArgumentSupplier(boundingBox, objectProperties, collectionSupplier)) + ); + } + + private static Object[] deserializerArgumentSupplier(GeoBoundingBox boundingBox, + Map properties, + BiFunction, ? extends GeoObject> geoSupplier) { + GeoObject geoObject = geoSupplier.apply(boundingBox, properties); + return new Object[]{GeoSerializationTestHelpers.geoToJson(geoObject), geoObject.getClass(), geoObject}; + } + + @Test + public void unknownGeoTypeSerializationThrows() { + Assertions.assertThrows(IOException.class, + () -> ADAPTER.serialize(new CustomGeoObject(null, null), SerializerEncoding.JSON)); + } + + private static final class CustomGeoObject extends GeoObject { + protected CustomGeoObject(GeoBoundingBox boundingBox, Map properties) { + super(boundingBox, properties); + } + + @Override + public GeoObjectType getType() { + return null; + } + } + + @ParameterizedTest + @MethodSource("geoJsonSerializationSupplier") + public void geoJsonSerialization(T geo, String expectedJson) throws IOException { + String actualJson = ADAPTER.serialize(geo, SerializerEncoding.JSON); + Assertions.assertEquals(expectedJson, actualJson); + } + + private static Stream geoJsonSerializationSupplier() { + GeoBoundingBox boundingBox = new GeoBoundingBox(0, 0, 1, 1, 0D, 1D); + Map simpleProperties = Collections.singletonMap("key", "value"); + Map arrayProperties = Collections.singletonMap("text", Arrays.asList("hello", "world")); + + Map crs = new HashMap<>(); + crs.put("type", "name"); + crs.put("properties", Collections.singletonMap("name", "EPSG:432")); + Map objectProperties = Collections.singletonMap("crs", crs); + + BiFunction, GeoPoint> pointSupplier = + (box, properties) -> new GeoPoint(new GeoPosition(0, 0, 0D), box, properties); + + List positions = Arrays.asList(new GeoPosition(0, 0, 1D), + new GeoPosition(1, 1, 1D)); + BiFunction, GeoLineString> lineSupplier = + (box, properties) -> new GeoLineString(positions, box, properties); + + List rings = Collections.singletonList(new GeoLinearRing(Arrays.asList( + new GeoPosition(0, 0, 1D), new GeoPosition(0, 1, 1D), + new GeoPosition(1, 1, 1D), new GeoPosition(0, 0, 1D) + ))); + BiFunction, GeoPolygon> polygonSupplier = + (box, properties) -> new GeoPolygon(rings, box, properties); + + BiFunction, GeoPointCollection> multiPointSupplier = + (box, properties) -> new GeoPointCollection(Arrays.asList(pointSupplier.apply(null, null), + pointSupplier.apply(box, properties)), box, properties); + + BiFunction, GeoLineStringCollection> multiLineSupplier = + (box, properties) -> new GeoLineStringCollection(Arrays.asList(lineSupplier.apply(null, null), + lineSupplier.apply(box, properties)), box, properties); + + BiFunction, GeoPolygonCollection> multiPolygonSuppluer = + (box, properties) -> new GeoPolygonCollection(Arrays.asList(polygonSupplier.apply(null, null), + polygonSupplier.apply(box, properties)), box, properties); + + BiFunction, GeoCollection> collectionSupplier = + (box, properties) -> new GeoCollection(Arrays.asList(pointSupplier.apply(null, null), + multiPointSupplier.apply(box, properties)), box, properties); + + return Stream.of( + // GeoPoint + Arguments.of(serializerArgumentSupplier(null, null, pointSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, pointSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, pointSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, pointSupplier)), + + // GeoLine + Arguments.of(serializerArgumentSupplier(null, null, lineSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, lineSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, lineSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, lineSupplier)), + + // GeoPolygon + Arguments.of(serializerArgumentSupplier(null, null, polygonSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, polygonSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, polygonSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, polygonSupplier)), + + // GeoPointCollection + Arguments.of(serializerArgumentSupplier(null, null, multiPointSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, multiPointSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, multiPointSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, multiPointSupplier)), + + // GeoLineCollection + Arguments.of(serializerArgumentSupplier(null, null, multiLineSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, multiLineSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, multiLineSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, multiLineSupplier)), + + // GeoPolygonCollection + Arguments.of(serializerArgumentSupplier(null, null, multiPolygonSuppluer)), + Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, multiPolygonSuppluer)), + Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, multiPolygonSuppluer)), + Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, multiPolygonSuppluer)), + + // GeoCollection + Arguments.of(serializerArgumentSupplier(null, null, collectionSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, simpleProperties, collectionSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, arrayProperties, collectionSupplier)), + Arguments.of(serializerArgumentSupplier(boundingBox, objectProperties, collectionSupplier)) + ); + } + + private static Object[] serializerArgumentSupplier(GeoBoundingBox boundingBox, Map properties, + BiFunction, ? extends GeoObject> geoSupplier) { + GeoObject geoObject = geoSupplier.apply(boundingBox, properties); + return new Object[]{geoObject, GeoSerializationTestHelpers.geoToJson(geoObject)}; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/FlatteningSerializerTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/FlatteningSerializerTests.java new file mode 100644 index 0000000000000..d1ef2bcad726e --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/FlatteningSerializerTests.java @@ -0,0 +1,765 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + + +import com.azure.core.implementation.TypeUtil; +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import com.azure.core.version.tests.models.AnimalShelter; +import com.azure.core.version.tests.models.AnimalWithTypeIdContainingDot; +import com.azure.core.version.tests.models.ClassWithFlattenedProperties; +import com.azure.core.version.tests.models.ComposeTurtles; +import com.azure.core.version.tests.models.DogWithTypeIdContainingDot; +import com.azure.core.version.tests.models.FlattenDangling; +import com.azure.core.version.tests.models.FlattenableAnimalInfo; +import com.azure.core.version.tests.models.FlattenedProduct; +import com.azure.core.version.tests.models.FlattenedPropertiesAndJsonAnyGetter; +import com.azure.core.version.tests.models.Foo; +import com.azure.core.version.tests.models.JsonFlattenNestedInner; +import com.azure.core.version.tests.models.JsonFlattenOnArrayType; +import com.azure.core.version.tests.models.JsonFlattenOnCollectionType; +import com.azure.core.version.tests.models.JsonFlattenOnJsonIgnoredProperty; +import com.azure.core.version.tests.models.JsonFlattenOnPrimitiveType; +import com.azure.core.version.tests.models.JsonFlattenWithJsonInfoDiscriminator; +import com.azure.core.version.tests.models.RabbitWithTypeIdContainingDot; +import com.azure.core.version.tests.models.SampleResource; +import com.azure.core.version.tests.models.School; +import com.azure.core.version.tests.models.Student; +import com.azure.core.version.tests.models.Teacher; +import com.azure.core.version.tests.models.TurtleWithTypeIdContainingDot; +import com.azure.core.version.tests.models.VirtualMachineIdentity; +import com.azure.core.version.tests.models.VirtualMachineScaleSet; +import com.azure.core.version.tests.models.VirtualMachineScaleSetNetworkConfiguration; +import com.azure.core.version.tests.models.VirtualMachineScaleSetNetworkProfile; +import com.azure.core.version.tests.models.VirtualMachineScaleSetVMProfile; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FlatteningSerializerTests { + private static final JacksonAdapter ADAPTER = new JacksonAdapter(); + + @Test + public void canFlatten() { + Foo foo = new Foo(); + foo.bar("hello.world"); + // + List baz = Arrays.asList("hello", "hello.world"); + foo.baz(baz); + + HashMap qux = new HashMap<>(); + qux.put("hello", "world"); + qux.put("a.b", "c.d"); + qux.put("bar.a", "ttyy"); + qux.put("bar.b", "uuzz"); + foo.qux(qux); + + // serialization + String serialized = serialize(foo); + assertEquals("{\"$type\":\"foo\",\"properties\":{\"bar\":\"hello.world\",\"props\":{\"baz\":[\"hello\",\"hello.world\"],\"q\":{\"qux\":{\"hello\":\"world\",\"a.b\":\"c.d\",\"bar.b\":\"uuzz\",\"bar.a\":\"ttyy\"}}}}}", serialized); + + // deserialization + Foo deserialized = deserialize(serialized, Foo.class); + assertEquals("hello.world", deserialized.bar()); + Assertions.assertArrayEquals(new String[]{"hello", "hello.world"}, deserialized.baz().toArray()); + assertNotNull(deserialized.qux()); + assertEquals("world", deserialized.qux().get("hello")); + assertEquals("c.d", deserialized.qux().get("a.b")); + assertEquals("ttyy", deserialized.qux().get("bar.a")); + assertEquals("uuzz", deserialized.qux().get("bar.b")); + } + + @Test + public void canSerializeMapKeysWithDotAndSlash() { + Teacher teacher = new Teacher(); + + Map students = new HashMap<>(); + students.put("af.B/C", new Student()); + students.put("af.B/D", new Student()); + + teacher.setStudents(students); + + School school = new School().setName("school1"); + school.setTeacher(teacher); + + Map schoolTags = new HashMap<>(); + schoolTags.put("foo.aa", "bar"); + schoolTags.put("x.y", "zz"); + + school.setTags(schoolTags); + + String serialized = serialize(school); + assertEquals("{\"teacher\":{\"students\":{\"af.B/D\":{},\"af.B/C\":{}}},\"tags\":{\"foo.aa\":\"bar\",\"x.y\":\"zz\"},\"properties\":{\"name\":\"school1\"}}", serialized); + } + + /** + * Validates decoding and encoding of a type with type id containing dot and no additional properties For decoding + * and encoding base type will be used. + */ + @Test + public void canHandleTypeWithTypeIdContainingDotAndNoProperties() { + String rabbitSerialized = "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}"; + String shelterSerialized = "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}},{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}}]}}"; + + AnimalWithTypeIdContainingDot rabbitDeserialized = deserialize(rabbitSerialized, + AnimalWithTypeIdContainingDot.class); + assertTrue(rabbitDeserialized instanceof RabbitWithTypeIdContainingDot); + assertNotNull(rabbitDeserialized); + + AnimalShelter shelterDeserialized = deserialize(shelterSerialized, AnimalShelter.class); + assertNotNull(shelterDeserialized); + assertEquals(2, shelterDeserialized.animalsInfo().size()); + for (FlattenableAnimalInfo animalInfo : shelterDeserialized.animalsInfo()) { + assertTrue(animalInfo.animal() instanceof RabbitWithTypeIdContainingDot); + assertNotNull(animalInfo.animal()); + } + } + + /** + * Validates that decoding and encoding of a type with type id containing dot and can be done. For decoding and + * encoding base type will be used. + */ + @Test + public void canHandleTypeWithTypeIdContainingDot0() { + List meals = Arrays.asList("carrot", "apple"); + // + AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + String serialized = serialize(animalToSerialize); + // + String[] results = { + "{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}", + "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}" + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + // De-Serialize + // + AnimalWithTypeIdContainingDot animalDeserialized = deserialize(serialized, AnimalWithTypeIdContainingDot.class); + assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot); + RabbitWithTypeIdContainingDot rabbit = (RabbitWithTypeIdContainingDot) animalDeserialized; + assertNotNull(rabbit.meals()); + assertEquals(rabbit.meals().size(), 2); + } + + /** + * Validates that decoding and encoding of a type with type id containing dot and can be done. For decoding and + * encoding concrete type will be used. + */ + @Test + public void canHandleTypeWithTypeIdContainingDot1() { + List meals = Arrays.asList("carrot", "apple"); + // + RabbitWithTypeIdContainingDot rabbitToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + String serialized = serialize(rabbitToSerialize); + // + String[] results = { + "{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}", + "{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}" + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + // De-Serialize + // + RabbitWithTypeIdContainingDot rabbitDeserialized = deserialize(serialized, RabbitWithTypeIdContainingDot.class); + assertNotNull(rabbitDeserialized); + assertNotNull(rabbitDeserialized.meals()); + assertEquals(rabbitDeserialized.meals().size(), 2); + } + + + /** + * Validates that decoding and encoding of a type with flattenable property and type id containing dot and can be + * done. For decoding and encoding base type will be used. + */ + @Test + public void canHandleTypeWithFlattenablePropertyAndTypeIdContainingDot0() { + AnimalWithTypeIdContainingDot animalToSerialize = new DogWithTypeIdContainingDot() + .withBreed("AKITA") + .withCuteLevel(10); + + // serialization + String serialized = serialize(animalToSerialize); + String[] results = { + "{\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10}}", + "{\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}", + "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10}}", + "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\"}", + "{\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\"}", + "{\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}", + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + // de-serialization + AnimalWithTypeIdContainingDot animalDeserialized = deserialize(serialized, AnimalWithTypeIdContainingDot.class); + assertTrue(animalDeserialized instanceof DogWithTypeIdContainingDot); + DogWithTypeIdContainingDot dogDeserialized = (DogWithTypeIdContainingDot) animalDeserialized; + assertNotNull(dogDeserialized); + assertEquals(dogDeserialized.breed(), "AKITA"); + assertEquals(dogDeserialized.cuteLevel(), (Integer) 10); + } + + /** + * Validates that decoding and encoding of a type with flattenable property and type id containing dot and can be + * done. For decoding and encoding concrete type will be used. + */ + @Test + public void canHandleTypeWithFlattenablePropertyAndTypeIdContainingDot1() { + DogWithTypeIdContainingDot dogToSerialize = new DogWithTypeIdContainingDot().withBreed("AKITA").withCuteLevel(10); + + // serialization + String serialized = serialize(dogToSerialize); + String[] results = { + "{\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10}}", + "{\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}", + "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\",\"properties\":{\"cuteLevel\":10}}", + "{\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\"}", + "{\"properties\":{\"cuteLevel\":10},\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\",\"breed\":\"AKITA\"}", + "{\"properties\":{\"cuteLevel\":10},\"breed\":\"AKITA\",\"@odata.type\":\"#Favourite.Pet.DogWithTypeIdContainingDot\"}", + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + // de-serialization + DogWithTypeIdContainingDot dogDeserialized = deserialize(serialized, DogWithTypeIdContainingDot.class); + assertNotNull(dogDeserialized); + assertEquals(dogDeserialized.breed(), "AKITA"); + assertEquals(dogDeserialized.cuteLevel(), (Integer) 10); + } + + /** + * Validates that decoding and encoding of a array of type with type id containing dot and can be done. For decoding + * and encoding base type will be used. + */ + @Test + public void canHandleArrayOfTypeWithTypeIdContainingDot0() { + List meals = Arrays.asList("carrot", "apple"); + // + AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + List animalsToSerialize = new ArrayList<>(); + animalsToSerialize.add(animalToSerialize); + String serialized = serialize(animalsToSerialize); + String[] results = { + "[{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}]", + "[{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}]", + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + // De-serialize + // + List animalsDeserialized = deserialize(serialized, + TypeUtil.createParameterizedType(List.class, AnimalWithTypeIdContainingDot.class)); + assertNotNull(animalsDeserialized); + assertEquals(1, animalsDeserialized.size()); + AnimalWithTypeIdContainingDot animalDeserialized = animalsDeserialized.get(0); + assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot); + RabbitWithTypeIdContainingDot rabbitDeserialized = (RabbitWithTypeIdContainingDot) animalDeserialized; + assertNotNull(rabbitDeserialized.meals()); + assertEquals(rabbitDeserialized.meals().size(), 2); + } + + /** + * Validates that decoding and encoding of a array of type with type id containing dot and can be done. For decoding + * and encoding concrete type will be used. + */ + @Test + public void canHandleArrayOfTypeWithTypeIdContainingDot1() { + List meals = Arrays.asList("carrot", "apple"); + // + RabbitWithTypeIdContainingDot rabbitToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + List rabbitsToSerialize = new ArrayList<>(); + rabbitsToSerialize.add(rabbitToSerialize); + String serialized = serialize(rabbitsToSerialize); + String[] results = { + "[{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}]", + "[{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}]", + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + // De-serialize + // + List rabbitsDeserialized = deserialize(serialized, + TypeUtil.createParameterizedType(List.class, RabbitWithTypeIdContainingDot.class)); + assertNotNull(rabbitsDeserialized); + assertEquals(1, rabbitsDeserialized.size()); + RabbitWithTypeIdContainingDot rabbitDeserialized = rabbitsDeserialized.get(0); + assertNotNull(rabbitDeserialized.meals()); + assertEquals(rabbitDeserialized.meals().size(), 2); + } + + + /** + * Validates that decoding and encoding of a composed type with type id containing dot and can be done. + */ + @Test + public void canHandleComposedTypeWithTypeIdContainingDot0() { + List meals = Arrays.asList("carrot", "apple"); + AnimalWithTypeIdContainingDot animalToSerialize = new RabbitWithTypeIdContainingDot().withMeals(meals); + FlattenableAnimalInfo animalInfoToSerialize = new FlattenableAnimalInfo().withAnimal(animalToSerialize); + List animalsInfoSerialized = Collections.singletonList(animalInfoToSerialize); + AnimalShelter animalShelterToSerialize = new AnimalShelter().withAnimalsInfo(animalsInfoSerialized); + String serialized = serialize(animalShelterToSerialize); + String[] results = { + "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"meals\":[\"carrot\",\"apple\"],\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\"}}]}}", + "{\"properties\":{\"animalsInfo\":[{\"animal\":{\"@odata.type\":\"#Favourite.Pet.RabbitWithTypeIdContainingDot\",\"meals\":[\"carrot\",\"apple\"]}}]}}", + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + // de-serialization + // + AnimalShelter shelterDeserialized = deserialize(serialized, AnimalShelter.class); + assertNotNull(shelterDeserialized.animalsInfo()); + assertEquals(shelterDeserialized.animalsInfo().size(), 1); + FlattenableAnimalInfo animalsInfoDeserialized = shelterDeserialized.animalsInfo().get(0); + assertTrue(animalsInfoDeserialized.animal() instanceof RabbitWithTypeIdContainingDot); + AnimalWithTypeIdContainingDot animalDeserialized = animalsInfoDeserialized.animal(); + assertTrue(animalDeserialized instanceof RabbitWithTypeIdContainingDot); + RabbitWithTypeIdContainingDot rabbitDeserialized = (RabbitWithTypeIdContainingDot) animalDeserialized; + assertNotNull(rabbitDeserialized); + assertNotNull(rabbitDeserialized.meals()); + assertEquals(rabbitDeserialized.meals().size(), 2); + } + + @Test + public void canHandleComposedSpecificPolymorphicTypeWithTypeId() { + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet1()); + assertEquals(2, composedTurtleDeserialized.turtlesSet1().size()); + // + serialize(composedTurtleDeserialized); + // + // -- Validate scalar property + // + String serializedScalarWithTypeId = "{\"turtlesSet1Lead\":{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}}"; + // de-serialization + // + composedTurtleDeserialized = deserialize(serializedScalarWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet1Lead()); + assertEquals(10, (long) composedTurtleDeserialized.turtlesSet1Lead().size()); + assertEquals(100, (long) composedTurtleDeserialized.turtlesSet1Lead().age()); + // + serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedSpecificPolymorphicTypeWithoutTypeId() { + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10 },{\"age\":200,\"size\":20 }]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet1()); + assertEquals(2, composedTurtleDeserialized.turtlesSet1().size()); + // + serialize(composedTurtleDeserialized); + // + // -- Validate scalar property + // + String serializedScalarWithTypeId = "{\"turtlesSet1Lead\":{\"age\":100,\"size\":10 }}"; + // de-serialization + // + composedTurtleDeserialized = deserialize(serializedScalarWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet1Lead()); + assertEquals(100, (long) composedTurtleDeserialized.turtlesSet1Lead().age()); + // + serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedSpecificPolymorphicTypeWithAndWithoutTypeId() { + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet1\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20 }]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet1()); + assertEquals(2, composedTurtleDeserialized.turtlesSet1().size()); + // + serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedGenericPolymorphicTypeWithTypeId() { + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet2()); + assertEquals(2, composedTurtleDeserialized.turtlesSet2().size()); + // + assertTrue(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot); + assertTrue(composedTurtleDeserialized.turtlesSet2().get(1) instanceof TurtleWithTypeIdContainingDot); + // + serialize(composedTurtleDeserialized); + // + // -- Validate scalar property + // + String serializedScalarWithTypeId = "{\"turtlesSet2Lead\":{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"}}"; + // de-serialization + // + composedTurtleDeserialized = deserialize(serializedScalarWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet2Lead()); + assertTrue(composedTurtleDeserialized.turtlesSet2Lead() instanceof TurtleWithTypeIdContainingDot); + assertEquals(10, (long) ((TurtleWithTypeIdContainingDot) composedTurtleDeserialized.turtlesSet2Lead()).size()); + assertEquals(100, (long) composedTurtleDeserialized.turtlesSet2Lead().age()); + // + serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedGenericPolymorphicTypeWithoutTypeId() { + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10 },{\"age\":200,\"size\":20 }]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet2()); + assertEquals(2, composedTurtleDeserialized.turtlesSet2().size()); + // + Assertions.assertFalse(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot); + Assertions.assertFalse(composedTurtleDeserialized.turtlesSet2().get(1) instanceof TurtleWithTypeIdContainingDot); + // + // -- Validate scalar property + // + serialize(composedTurtleDeserialized); + // + String serializedScalarWithTypeId = "{\"turtlesSet2Lead\":{\"age\":100,\"size\":10 }}"; + // de-serialization + // + composedTurtleDeserialized = deserialize(serializedScalarWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet2Lead()); + // + serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleComposedGenericPolymorphicTypeWithAndWithoutTypeId() { + // + // -- Validate vector property + // + String serializedCollectionWithTypeId = "{\"turtlesSet2\":[{\"age\":100,\"size\":10,\"@odata.type\":\"#Favourite.Pet.TurtleWithTypeIdContainingDot\"},{\"age\":200,\"size\":20 }]}"; + // de-serialization + // + ComposeTurtles composedTurtleDeserialized = deserialize(serializedCollectionWithTypeId, ComposeTurtles.class); + assertNotNull(composedTurtleDeserialized); + assertNotNull(composedTurtleDeserialized.turtlesSet2()); + assertEquals(2, composedTurtleDeserialized.turtlesSet2().size()); + // + assertTrue(composedTurtleDeserialized.turtlesSet2().get(0) instanceof TurtleWithTypeIdContainingDot); + assertNotNull(composedTurtleDeserialized.turtlesSet2().get(1)); + // + serialize(composedTurtleDeserialized); + } + + @Test + public void canHandleEscapedProperties() { + FlattenedProduct productToSerialize = new FlattenedProduct() + .setProductName("drink") + .setProductType("chai"); + + // serialization + // + String serialized = serialize(productToSerialize); + String[] results = { + "{\"properties\":{\"p.name\":\"drink\",\"type\":\"chai\"}}", + "{\"properties\":{\"type\":\"chai\",\"p.name\":\"drink\"}}", + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + // de-serialization + // + FlattenedProduct productDeserialized = deserialize(serialized, FlattenedProduct.class); + assertNotNull(productDeserialized); + assertEquals(productDeserialized.getProductName(), "drink"); + assertEquals(productDeserialized.getProductType(), "chai"); + } + + @Test + public void canHandleSinglePropertyBeingFlattened() { + ClassWithFlattenedProperties classWithFlattenedProperties = new ClassWithFlattenedProperties("random", "E24JJxztP"); + + String serialized = serialize(classWithFlattenedProperties); + String[] results = { + "{\"@odata\":{\"type\":\"random\"},\"@odata.etag\":\"E24JJxztP\"}", + "{\"@odata.etag\":\"E24JJxztP\",\"@odata\":{\"type\":\"random\"}}" + }; + + assertTrue(Arrays.asList(results).contains(serialized)); + + ClassWithFlattenedProperties deserialized = deserialize(serialized, ClassWithFlattenedProperties.class); + assertNotNull(deserialized); + assertEquals(classWithFlattenedProperties.getOdataType(), deserialized.getOdataType()); + assertEquals(classWithFlattenedProperties.getOdataETag(), deserialized.getOdataETag()); + } + + @Test + public void canHandleMultiLevelPropertyFlattening() { + VirtualMachineScaleSet virtualMachineScaleSet = new VirtualMachineScaleSet() + .setVirtualMachineProfile(new VirtualMachineScaleSetVMProfile() + .setNetworkProfile(new VirtualMachineScaleSetNetworkProfile() + .setNetworkInterfaceConfigurations(Collections.singletonList( + new VirtualMachineScaleSetNetworkConfiguration().setName("name").setPrimary(true))))); + + String serialized = serialize(virtualMachineScaleSet); + String expected = "{\"properties\":{\"virtualMachineProfile\":{\"networkProfile\":{\"networkInterfaceConfigurations\":[{\"name\":\"name\",\"properties\":{\"primary\":true}}]}}}}"; + assertEquals(expected, serialized); + + VirtualMachineScaleSet deserialized = deserialize(serialized, VirtualMachineScaleSet.class); + assertNotNull(deserialized); + + VirtualMachineScaleSetNetworkConfiguration expectedConfig = virtualMachineScaleSet.getVirtualMachineProfile() + .getNetworkProfile() + .getNetworkInterfaceConfigurations() + .get(0); + + VirtualMachineScaleSetNetworkConfiguration actualConfig = deserialized.getVirtualMachineProfile() + .getNetworkProfile() + .getNetworkInterfaceConfigurations() + .get(0); + + assertEquals(expectedConfig.getName(), actualConfig.getName()); + assertEquals(expectedConfig.getPrimary(), actualConfig.getPrimary()); + } + + @Test + public void jsonFlattenOnArrayType() { + JsonFlattenOnArrayType expected = new JsonFlattenOnArrayType() + .setJsonFlattenArray(new String[]{"hello", "goodbye", null}); + + String expectedSerialization = "{\"jsonflatten\":{\"array\":[\"hello\",\"goodbye\",null]}}"; + String actualSerialization = serialize(expected); + + assertEquals(expectedSerialization, actualSerialization); + + JsonFlattenOnArrayType deserialized = deserialize(actualSerialization, JsonFlattenOnArrayType.class); + assertArrayEquals(expected.getJsonFlattenArray(), deserialized.getJsonFlattenArray()); + } + + @Test + public void jsonFlattenOnCollectionTypeList() { + final List listCollection = Arrays.asList("hello", "goodbye", null); + JsonFlattenOnCollectionType expected = new JsonFlattenOnCollectionType() + .setJsonFlattenCollection(Collections.unmodifiableList(listCollection)); + + String expectedSerialization = "{\"jsonflatten\":{\"collection\":[\"hello\",\"goodbye\",null]}}"; + String actualSerialization = serialize(expected); + + assertEquals(expectedSerialization, actualSerialization); + + JsonFlattenOnCollectionType deserialized = deserialize(actualSerialization, JsonFlattenOnCollectionType.class); + assertEquals(expected.getJsonFlattenCollection().size(), deserialized.getJsonFlattenCollection().size()); + for (int i = 0; i < expected.getJsonFlattenCollection().size(); i++) { + assertEquals(expected.getJsonFlattenCollection().get(i), deserialized.getJsonFlattenCollection().get(i)); + } + } + + @Test + public void jsonFlattenOnJsonIgnoredProperty() { + JsonFlattenOnJsonIgnoredProperty expected = new JsonFlattenOnJsonIgnoredProperty() + .setName("name") + .setIgnored("ignored"); + + String expectedSerialization = "{\"name\":\"name\"}"; + String actualSerialization = serialize(expected); + + assertEquals(expectedSerialization, actualSerialization); + + JsonFlattenOnJsonIgnoredProperty deserialized = deserialize(actualSerialization, + JsonFlattenOnJsonIgnoredProperty.class); + assertEquals(expected.getName(), deserialized.getName()); + assertNull(deserialized.getIgnored()); + } + + @Test + public void jsonFlattenOnPrimitiveType() { + JsonFlattenOnPrimitiveType expected = new JsonFlattenOnPrimitiveType() + .setJsonFlattenBoolean(true) + .setJsonFlattenDecimal(1.25D) + .setJsonFlattenNumber(2) + .setJsonFlattenString("string"); + + String expectedSerialization = "{\"jsonflatten\":{\"boolean\":true,\"decimal\":1.25,\"number\":2,\"string\":\"string\"}}"; + String actualSerialization = serialize(expected); + + assertEquals(expectedSerialization, actualSerialization); + + JsonFlattenOnPrimitiveType deserialized = deserialize(actualSerialization, JsonFlattenOnPrimitiveType.class); + assertEquals(expected.isJsonFlattenBoolean(), deserialized.isJsonFlattenBoolean()); + assertEquals(expected.getJsonFlattenDecimal(), deserialized.getJsonFlattenDecimal()); + assertEquals(expected.getJsonFlattenNumber(), deserialized.getJsonFlattenNumber()); + assertEquals(expected.getJsonFlattenString(), deserialized.getJsonFlattenString()); + } + + @Test + public void jsonFlattenWithJsonInfoDiscriminator() { + JsonFlattenWithJsonInfoDiscriminator expected = new JsonFlattenWithJsonInfoDiscriminator() + .setJsonFlattenDiscriminator("discriminator"); + + String expectedSerialization = "{\"type\":\"JsonFlattenWithJsonInfoDiscriminator\",\"jsonflatten\":{\"discriminator\":\"discriminator\"}}"; + String actualSerialization = serialize(expected); + + assertEquals(expectedSerialization, actualSerialization); + + JsonFlattenWithJsonInfoDiscriminator deserialized = deserialize(actualSerialization, + JsonFlattenWithJsonInfoDiscriminator.class); + assertEquals(expected.getJsonFlattenDiscriminator(), deserialized.getJsonFlattenDiscriminator()); + } + + @Test + public void flattenedPropertiesAndJsonAnyGetter() { + FlattenedPropertiesAndJsonAnyGetter expected = new FlattenedPropertiesAndJsonAnyGetter() + .setString("string") + .addAdditionalProperty("key1", "value1") + .addAdditionalProperty("key2", "value2"); + + String expectedSerialization = "{\"flattened\":{\"string\":\"string\"},\"key1\":\"value1\",\"key2\":\"value2\"}"; + String actualSerialization = serialize(expected); + + assertEquals(expectedSerialization, actualSerialization); + + FlattenedPropertiesAndJsonAnyGetter deserialized = deserialize(actualSerialization, + FlattenedPropertiesAndJsonAnyGetter.class); + assertEquals(expected.getString(), deserialized.getString()); + assertEquals(expected.additionalProperties().size(), deserialized.additionalProperties().size()); + for (String key : expected.additionalProperties().keySet()) { + assertEquals(expected.additionalProperties().get(key), deserialized.additionalProperties().get(key)); + } + } + + @Test + public void jsonFlattenFinalMap() { + final HashMap mapProperties = new HashMap() {{ + put("/subscriptions/0-0-0-0-0/resourcegroups/0/providers/Microsoft.ManagedIdentity/0", "value"); + }}; + School school = new School().setTags(mapProperties); + + String actualSerialization = serialize(school); + String expectedSerialization = "{\"tags\":{\"/subscriptions/0-0-0-0-0/resourcegroups" + + "/0/providers/Microsoft.ManagedIdentity/0\":\"value\"}}"; + Assertions.assertEquals(expectedSerialization, actualSerialization); + } + + @Test + public void jsonFlattenNestedInner() { + JsonFlattenNestedInner expected = new JsonFlattenNestedInner(); + VirtualMachineIdentity identity = new VirtualMachineIdentity(); + final Map map = new HashMap<>(); + map.put("/subscriptions/0-0-0-0-0/resourcegroups/0/providers/Microsoft.ManagedIdentity/userAssignedIdentities/0", + new Object()); + identity.setType(Arrays.asList("SystemAssigned, UserAssigned")); + identity.setUserAssignedIdentities(map); + expected.setIdentity(identity); + + String expectedSerialization = "{\"identity\":{\"type\":[\"SystemAssigned, UserAssigned\"]," + + "\"userAssignedIdentities\":{\"/subscriptions/0-0-0-0-0/resourcegroups/0/providers/" + + "Microsoft.ManagedIdentity/userAssignedIdentities/0\":{}}}}"; + String actualSerialization = serialize(expected); + + Assertions.assertEquals(expectedSerialization, actualSerialization); + } + + @Test + public void jsonFlattenRepeatedPropertyNameDeserialize() throws IOException { + SampleResource deserialized = JacksonAdapter.createDefaultSerializerAdapter().deserialize( + "{\"name\":\"...-01\",\"properties\":{\"registrationTtl\":\"10675199.02:48:05.4775807\",\"authorizationRules\":[]}}", + SampleResource.class, + SerializerEncoding.JSON + ); + + assertEquals("10675199.02:48:05.4775807", deserialized.getRegistrationTtl()); + assertNull(deserialized.getNamePropertiesName()); + } + + @ParameterizedTest + @MethodSource("emptyDanglingNodeJsonSupplier") + public void jsonFlattenEmptyDanglingNodesDeserialize(String json, Object expected) throws IOException { + // test to verify null dangling nodes are still retained and set to null + FlattenDangling deserialized = JacksonAdapter.createDefaultSerializerAdapter().deserialize( + json, + FlattenDangling.class, + SerializerEncoding.JSON + ); + + assertEquals(expected, deserialized.getFlattenedProperty()); + } + + private static String serialize(Object object) { + try { + return ADAPTER.serialize(object, SerializerEncoding.JSON); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static T deserialize(String json, Type type) { + try { + return ADAPTER.deserialize(json, type, SerializerEncoding.JSON); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private static Stream emptyDanglingNodeJsonSupplier() { + return Stream.of( + Arguments.of("{\"a\":{}}", null), + + Arguments.of("{\"a\":{\"flattened\": {}}}", null), + + Arguments.of("{\"a\":{\"flattened\": {\"property\": null}}}", null), + + Arguments.of("{\"a\":{\"flattened\": {\"property\": \"value\"}}}", "value") + ); + } +} + diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/GeoSerializationTestHelpers.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/GeoSerializationTestHelpers.java new file mode 100644 index 0000000000000..711240bc6ae29 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/GeoSerializationTestHelpers.java @@ -0,0 +1,277 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.implementation.GeoObjectHelper; +import com.azure.core.models.GeoBoundingBox; +import com.azure.core.models.GeoCollection; +import com.azure.core.models.GeoLineString; +import com.azure.core.models.GeoLineStringCollection; +import com.azure.core.models.GeoLinearRing; +import com.azure.core.models.GeoObject; +import com.azure.core.models.GeoObjectType; +import com.azure.core.models.GeoPoint; +import com.azure.core.models.GeoPointCollection; +import com.azure.core.models.GeoPolygon; +import com.azure.core.models.GeoPolygonCollection; +import com.azure.core.models.GeoPosition; +import com.azure.core.util.CoreUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Test helpers for {@code GeoJsonDeserializerTests} and {@code GeoJsonSerializerTests}. + */ +public class GeoSerializationTestHelpers { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + static String geoToJson(GeoObject geoObject) { + if (geoObject instanceof GeoPoint) { + return pointToJson((GeoPoint) geoObject); + } else if (geoObject instanceof GeoLineString) { + return lineToJson((GeoLineString) geoObject); + } else if (geoObject instanceof GeoPolygon) { + return polygonToJson((GeoPolygon) geoObject); + } else if (geoObject instanceof GeoPointCollection) { + return multiPointToJson((GeoPointCollection) geoObject); + } else if (geoObject instanceof GeoLineStringCollection) { + return multiLineToJson((GeoLineStringCollection) geoObject); + } else if (geoObject instanceof GeoPolygonCollection) { + return multiPolygonToJson((GeoPolygonCollection) geoObject); + } else if (geoObject instanceof GeoCollection) { + return collectionToJson((GeoCollection) geoObject); + } else { + throw new IllegalStateException("Unknown geo type."); + } + } + + private static String pointToJson(GeoPoint point) { + StringBuilder builder = new StringBuilder("{"); + addType(GeoObjectType.POINT, builder); + + builder.append(",\"coordinates\":"); + addPosition(point.getCoordinates(), builder); + + addAdditionalProperties(point, builder); + + builder.append("}"); + + return builder.toString(); + } + + private static String lineToJson(GeoLineString line) { + StringBuilder builder = new StringBuilder("{"); + addType(GeoObjectType.LINE_STRING, builder); + + builder.append(",\"coordinates\":"); + addLine(line.getCoordinates(), builder); + + addAdditionalProperties(line, builder); + + builder.append("}"); + + return builder.toString(); + } + + private static String polygonToJson(GeoPolygon polygon) { + StringBuilder builder = new StringBuilder("{"); + addType(GeoObjectType.POLYGON, builder); + + builder.append(",\"coordinates\":"); + addPolygon(polygon.getRings(), builder); + + addAdditionalProperties(polygon, builder); + + builder.append("}"); + + return builder.toString(); + } + + private static String multiPointToJson(GeoPointCollection multiPoint) { + StringBuilder builder = new StringBuilder("{"); + addType(GeoObjectType.MULTI_POINT, builder); + + builder.append(",\"coordinates\":"); + addLine(multiPoint.getPoints().stream().map(GeoPoint::getCoordinates).collect(Collectors.toList()), builder); + + addAdditionalProperties(multiPoint, builder); + + builder.append("}"); + + return builder.toString(); + + } + + private static String multiLineToJson(GeoLineStringCollection multiLine) { + StringBuilder builder = new StringBuilder("{"); + addType(GeoObjectType.MULTI_LINE_STRING, builder); + + builder.append(",\"coordinates\":["); + for (int i = 0; i < multiLine.getLines().size(); i++) { + if (i > 0) { + builder.append(","); + } + + addLine(multiLine.getLines().get(i).getCoordinates(), builder); + } + + builder.append("]"); + + addAdditionalProperties(multiLine, builder); + + builder.append("}"); + + return builder.toString(); + } + + private static String multiPolygonToJson(GeoPolygonCollection multiPolygon) { + StringBuilder builder = new StringBuilder("{"); + addType(GeoObjectType.MULTI_POLYGON, builder); + + builder.append(",\"coordinates\":["); + + for (int i = 0; i < multiPolygon.getPolygons().size(); i++) { + if (i > 0) { + builder.append(","); + } + + addPolygon(multiPolygon.getPolygons().get(i).getRings(), builder); + } + + builder.append("]"); + + addAdditionalProperties(multiPolygon, builder); + + builder.append("}"); + + return builder.toString(); + } + + private static String collectionToJson(GeoCollection collection) { + StringBuilder builder = new StringBuilder("{"); + addType(GeoObjectType.GEOMETRY_COLLECTION, builder); + + builder.append(",\"geometries\":["); + + for (int i = 0; i < collection.getGeometries().size(); i++) { + if (i > 0) { + builder.append(","); + } + + builder.append(geoToJson(collection.getGeometries().get(i))); + } + + builder.append("]"); + + addAdditionalProperties(collection, builder); + + builder.append("}"); + + return builder.toString(); + } + + private static void addType(GeoObjectType type, StringBuilder builder) { + builder.append("\"type\":\"").append(type.toString()).append("\""); + } + + private static void addPosition(GeoPosition position, StringBuilder builder) { + builder.append("[") + .append(position.getLongitude()) + .append(",") + .append(position.getLatitude()); + + Double altitude = position.getAltitude(); + if (altitude != null) { + builder.append(",").append(altitude); + } + + builder.append("]"); + } + + private static void addLine(List positions, StringBuilder builder) { + builder.append("["); + + for (int i = 0; i < positions.size(); i++) { + if (i > 0) { + builder.append(","); + } + + addPosition(positions.get(i), builder); + } + + builder.append("]"); + } + + private static void addPolygon(List rings, StringBuilder builder) { + builder.append("["); + + for (int i = 0; i < rings.size(); i++) { + if (i > 0) { + builder.append(","); + } + + addLine(rings.get(i).getCoordinates(), builder); + } + + builder.append("]"); + } + + private static void addAdditionalProperties(GeoObject geoObject, StringBuilder builder) { + addBoundingBox(geoObject.getBoundingBox(), builder); + addProperties(GeoObjectHelper.getCustomProperties(geoObject), builder); + } + + private static void addBoundingBox(GeoBoundingBox boundingBox, StringBuilder builder) { + if (boundingBox == null) { + return; + } + + builder.append(",\"bbox\":[") + .append(boundingBox.getWest()) + .append(",") + .append(boundingBox.getSouth()) + .append(","); + + Double minAltitude = boundingBox.getMinAltitude(); + if (minAltitude != null) { + builder.append(minAltitude).append(","); + } + + builder.append(boundingBox.getEast()) + .append(",") + .append(boundingBox.getNorth()); + + Double maxAltitude = boundingBox.getMaxAltitude(); + if (maxAltitude != null) { + builder.append(",").append(maxAltitude); + } + + builder.append("]"); + } + + private static void addProperties(Map properties, StringBuilder builder) { + if (CoreUtils.isNullOrEmpty(properties)) { + return; + } + + try { + for (Map.Entry property : properties.entrySet()) { + if (builder.length() > 0) { + builder.append(","); + } + + builder.append("\"") + .append(property.getKey()) + .append("\":") + .append(MAPPER.writeValueAsString(property.getValue())); + } + } catch (JsonProcessingException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JacksonAdapterTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JacksonAdapterTests.java new file mode 100644 index 0000000000000..b8288e0f24ff9 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JacksonAdapterTests.java @@ -0,0 +1,407 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpMethod; +import com.azure.core.implementation.AccessibleByteArrayOutputStream; +import com.azure.core.models.GeoObjectType; +import com.azure.core.models.JsonPatchDocument; +import com.azure.core.test.utils.TestUtils; +import com.azure.core.util.DateTimeRfc1123; +import com.azure.core.util.UrlBuilder; +import com.azure.core.util.serializer.CollectionFormat; +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class JacksonAdapterTests { + private static final JacksonAdapter ADAPTER = new JacksonAdapter(); + + @Test + public void emptyMap() throws IOException { + final Map map = new HashMap<>(); + assertEquals("{}", ADAPTER.serialize(map, SerializerEncoding.JSON)); + } + + @Test + public void mapWithNullKey() throws IOException { + final Map map = new HashMap<>(); + map.put(null, null); + assertEquals("{}", ADAPTER.serialize(map, SerializerEncoding.JSON)); + } + + @Test + public void mapWithEmptyKeyAndNullValue() throws IOException { + final MapHolder mapHolder = new MapHolder(); + mapHolder.map(new HashMap<>()); + mapHolder.map().put("", null); + + assertEquals("{\"map\":{\"\":null}}", ADAPTER.serialize(mapHolder, SerializerEncoding.JSON)); + } + + @Test + public void mapWithEmptyKeyAndEmptyValue() throws IOException { + final MapHolder mapHolder = new MapHolder(); + mapHolder.map = new HashMap<>(); + mapHolder.map.put("", ""); + + assertEquals("{\"map\":{\"\":\"\"}}", ADAPTER.serialize(mapHolder, SerializerEncoding.JSON)); + } + + @Test + public void mapWithEmptyKeyAndNonEmptyValue() throws IOException { + final Map map = new HashMap<>(); + map.put("", "test"); + + assertEquals("{\"\":\"test\"}", ADAPTER.serialize(map, SerializerEncoding.JSON)); + } + + private static class MapHolder { + + @JsonInclude(content = JsonInclude.Include.ALWAYS) + private Map map = new HashMap<>(); + + public Map map() { + return map; + } + + public void map(Map map) { + this.map = map; + } + + } + + @JacksonXmlRootElement(localName = "XmlString") + private static class XmlString { + + @JsonProperty("Value") + private String value; + + public String getValue() { + return value; + } + + } + + @ParameterizedTest + @MethodSource("serializeCollectionSupplier") + public void testSerializeList(List values, CollectionFormat format, String expectedSerializedString) { + String actualSerializedString = JacksonAdapter.createDefaultSerializerAdapter() + .serializeList(values, format); + assertEquals(expectedSerializedString, actualSerializedString); + } + + @ParameterizedTest + @MethodSource("serializeCollectionSupplier") + public void testSerializeIterable(Iterable values, CollectionFormat format, String expectedSerializedString) { + String actualSerializedString = JacksonAdapter.createDefaultSerializerAdapter() + .serializeIterable(values, format); + assertEquals(expectedSerializedString, actualSerializedString); + } + + @ParameterizedTest + @MethodSource("deserializeJsonSupplier") + public void deserializeJson(String json, OffsetDateTime expected) throws IOException { + DateTimeWrapper wrapper = JacksonAdapter.createDefaultSerializerAdapter() + .deserialize(json, DateTimeWrapper.class, SerializerEncoding.JSON); + + assertEquals(expected, wrapper.getOffsetDateTime()); + } + + private static Stream serializeCollectionSupplier() { + return Stream.of( + Arguments.of(Arrays.asList("foo", "bar", "baz"), CollectionFormat.CSV, "foo,bar,baz"), + Arguments.of(Arrays.asList("foo", null, "baz"), CollectionFormat.CSV, "foo,,baz"), + Arguments.of(Arrays.asList(null, "bar", null, null), CollectionFormat.CSV, ",bar,,"), + Arguments.of(Arrays.asList(1, 2, 3), CollectionFormat.CSV, "1,2,3"), + Arguments.of(Arrays.asList(1, 2, 3), CollectionFormat.PIPES, "1|2|3"), + Arguments.of(Arrays.asList(1, 2, 3), CollectionFormat.SSV, "1 2 3"), + Arguments.of(Arrays.asList("foo", "bar", "baz"), CollectionFormat.MULTI, "foo&bar&baz") + ); + } + + private static Stream deserializeJsonSupplier() { + final String jsonFormat = "{\"OffsetDateTime\":\"%s\"}"; + OffsetDateTime minValue = OffsetDateTime.of(1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + OffsetDateTime unixEpoch = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + + return Stream.of( + Arguments.of(String.format(jsonFormat, "0001-01-01T00:00:00"), minValue), + Arguments.of(String.format(jsonFormat, "0001-01-01T00:00:00Z"), minValue), + Arguments.of(String.format(jsonFormat, "1970-01-01T00:00:00"), unixEpoch), + Arguments.of(String.format(jsonFormat, "1970-01-01T00:00:00Z"), unixEpoch) + ); + } + + @ParameterizedTest + @MethodSource("deserializeXmlSupplier") + public void deserializeXml(String xml, OffsetDateTime expected) throws IOException { + DateTimeWrapper wrapper = JacksonAdapter.createDefaultSerializerAdapter() + .deserialize(xml, DateTimeWrapper.class, SerializerEncoding.XML); + + assertEquals(expected, wrapper.getOffsetDateTime()); + } + + private static Stream deserializeXmlSupplier() { + final String xmlFormat = "%s"; + OffsetDateTime minValue = OffsetDateTime.of(1, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + OffsetDateTime unixEpoch = OffsetDateTime.of(1970, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + + return Stream.of( + Arguments.of(String.format(xmlFormat, "0001-01-01T00:00:00"), minValue), + Arguments.of(String.format(xmlFormat, "0001-01-01T00:00:00Z"), minValue), + Arguments.of(String.format(xmlFormat, "1970-01-01T00:00:00"), unixEpoch), + Arguments.of(String.format(xmlFormat, "1970-01-01T00:00:00Z"), unixEpoch) + ); + } + + @JacksonXmlRootElement(localName = "Wrapper") + private static class DateTimeWrapper { + @JsonProperty(value = "OffsetDateTime", required = true) + private OffsetDateTime offsetDateTime; + + public DateTimeWrapper setOffsetDateTime(OffsetDateTime offsetDateTime) { + this.offsetDateTime = offsetDateTime; + return this; + } + + public OffsetDateTime getOffsetDateTime() { + return offsetDateTime; + } + } + + @Test + public void stronglyTypedHeadersClassIsDeserialized() throws IOException { + final String expectedDate = DateTimeRfc1123.toRfc1123String(OffsetDateTime.now()); + + HttpHeaders rawHeaders = new HttpHeaders().set(HttpHeaderName.DATE, expectedDate); + + StronglyTypedHeaders stronglyTypedHeaders = JacksonAdapter.createDefaultSerializerAdapter() + .deserialize(rawHeaders, StronglyTypedHeaders.class); + + assertEquals(expectedDate, DateTimeRfc1123.toRfc1123String(stronglyTypedHeaders.getDate())); + } + + @Test + public void stronglyTypedHeadersClassThrowsEagerly() { + HttpHeaders rawHeaders = new HttpHeaders().set(HttpHeaderName.DATE, "invalid-rfc1123-date"); + + assertThrows(DateTimeParseException.class, () -> JacksonAdapter.createDefaultSerializerAdapter() + .deserialize(rawHeaders, StronglyTypedHeaders.class)); + } + + @Test + public void invalidStronglyTypedHeadersClassThrowsCorrectException() throws IOException { + try { + JacksonAdapter.createDefaultSerializerAdapter().deserialize(new HttpHeaders(), + InvalidStronglyTypedHeaders.class); + + fail("An exception should have been thrown."); + } catch (RuntimeException ex) { + assertTrue(ex.getCause() instanceof JsonProcessingException, "Exception cause type was " + + ex.getCause().getClass().getName() + " instead of the expected JsonProcessingException type."); + } + } + + @ParameterizedTest + @MethodSource("textSerializationSupplier") + public void textToStringSerialization(Object value, String expected) throws IOException { + assertEquals(expected, ADAPTER.serialize(value, SerializerEncoding.TEXT)); + } + + @ParameterizedTest + @MethodSource("textSerializationSupplier") + public void textToBytesSerialization(Object value, String expected) throws IOException { + byte[] actual = ADAPTER.serializeToBytes(value, SerializerEncoding.TEXT); + + if (expected == null) { + assertNull(actual); + } else { + assertEquals(expected, new String(actual, StandardCharsets.UTF_8)); + } + } + + @ParameterizedTest + @MethodSource("textSerializationSupplier") + public void textToOutputStreamSerialization(Object value, String expected) throws IOException { + AccessibleByteArrayOutputStream outputStream = new AccessibleByteArrayOutputStream(); + ADAPTER.serialize(value, SerializerEncoding.TEXT, outputStream); + + if (expected == null) { + assertEquals(0, outputStream.count()); + } else { + assertEquals(expected, outputStream.toString(StandardCharsets.UTF_8)); + } + } + + private static Stream textSerializationSupplier() { + Map map = Collections.singletonMap("key", "value"); + + return Stream.of( + Arguments.of(1, "1"), + Arguments.of(1L, "1"), + Arguments.of(1.0F, "1.0"), + Arguments.of(1.0D, "1.0"), + Arguments.of("1", "1"), + Arguments.of(HttpMethod.GET, "GET"), + Arguments.of(GeoObjectType.POINT, "Point"), + Arguments.of(map, String.valueOf(map)), + Arguments.of(null, null) + ); + } + + @ParameterizedTest + @MethodSource("textDeserializationSupplier") + public void stringToTextDeserialization(byte[] stringBytes, Class type, Object expected) throws IOException { + Object actual = ADAPTER.deserialize(new String(stringBytes, StandardCharsets.UTF_8), type, + SerializerEncoding.TEXT); + + if (type == byte[].class) { + TestUtils.assertArraysEqual((byte[]) expected, (byte[]) actual); + } else { + assertEquals(expected, actual); + } + } + + @ParameterizedTest + @MethodSource("textDeserializationSupplier") + public void bytesToTextDeserialization(byte[] bytes, Class type, Object expected) throws IOException { + Object actual = ADAPTER.deserialize(bytes, type, SerializerEncoding.TEXT); + + if (type == byte[].class) { + TestUtils.assertArraysEqual((byte[]) expected, (byte[]) actual); + } else { + assertEquals(expected, actual); + } + } + + @ParameterizedTest + @MethodSource("textDeserializationSupplier") + public void inputStreamToTextDeserialization(byte[] inputStreamBytes, Class type, Object expected) + throws IOException { + Object actual = ADAPTER.deserialize(new ByteArrayInputStream(inputStreamBytes), type, SerializerEncoding.TEXT); + + if (type == byte[].class) { + TestUtils.assertArraysEqual((byte[]) expected, (byte[]) actual); + } else { + assertEquals(expected, actual); + } + } + + private static Stream textDeserializationSupplier() throws MalformedURLException { + byte[] helloBytes = "hello".getBytes(StandardCharsets.UTF_8); + String urlUri = "https://azure.com"; + byte[] urlUriBytes = urlUri.getBytes(StandardCharsets.UTF_8); + OffsetDateTime offsetDateTime = OffsetDateTime.now(ZoneOffset.UTC).truncatedTo(ChronoUnit.SECONDS); + DateTimeRfc1123 dateTimeRfc1123 = new DateTimeRfc1123(offsetDateTime); + LocalDate localDate = LocalDate.now(ZoneOffset.UTC); + UUID uuid = UUID.randomUUID(); + HttpMethod httpMethod = HttpMethod.GET; + GeoObjectType geoObjectType = GeoObjectType.POINT; + + return Stream.of( + Arguments.of(helloBytes, String.class, "hello"), + Arguments.of(helloBytes, CharSequence.class, "hello"), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), int.class, 1), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), Integer.class, 1), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), byte.class, (byte) 49), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), Byte.class, (byte) 49), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), long.class, 1L), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), Long.class, 1L), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), short.class, (short) 1), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), Short.class, (short) 1), + Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), double.class, 1.0D), + Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), Double.class, 1.0D), + Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), float.class, 1.0F), + Arguments.of("1.0".getBytes(StandardCharsets.UTF_8), Float.class, 1.0F), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), char.class, '1'), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), Character.class, '1'), + Arguments.of("1".getBytes(StandardCharsets.UTF_8), byte[].class, "1".getBytes(StandardCharsets.UTF_8)), + Arguments.of("true".getBytes(StandardCharsets.UTF_8), boolean.class, true), + Arguments.of("true".getBytes(StandardCharsets.UTF_8), Boolean.class, true), + Arguments.of(urlUriBytes, URL.class, UrlBuilder.parse(urlUri).toUrl()), + Arguments.of(urlUriBytes, URI.class, URI.create(urlUri)), + Arguments.of(getObjectBytes(offsetDateTime), OffsetDateTime.class, offsetDateTime), + Arguments.of(getObjectBytes(dateTimeRfc1123), DateTimeRfc1123.class, dateTimeRfc1123), + Arguments.of(getObjectBytes(localDate), LocalDate.class, localDate), + Arguments.of(getObjectBytes(uuid), UUID.class, uuid), + Arguments.of(getObjectBytes(httpMethod), HttpMethod.class, httpMethod), + Arguments.of(getObjectBytes(geoObjectType), GeoObjectType.class, geoObjectType) + ); + } + + @ParameterizedTest + @MethodSource("textUnsupportedDeserializationSupplier") + public void unsupportedTextTypesDeserialization(Class unsupportedType, + Class exceptionType) { + assertThrows(exceptionType, () -> ADAPTER.deserialize(":////", unsupportedType, SerializerEncoding.TEXT)); + } + + private static Stream textUnsupportedDeserializationSupplier() { + return Stream.of( + Arguments.of(InputStream.class, IllegalStateException.class), + Arguments.of(JsonPatchDocument.class, IllegalStateException.class), + Arguments.of(URL.class, IOException.class), // Thrown when the String isn't a valid URL + Arguments.of(URI.class, IllegalArgumentException.class) // Thrown when the String isn't a valid URI + ); + } + + public static final class StronglyTypedHeaders { + private final DateTimeRfc1123 date; + + public StronglyTypedHeaders(HttpHeaders rawHeaders) { + String dateString = rawHeaders.getValue(HttpHeaderName.DATE); + this.date = (dateString == null) ? null : new DateTimeRfc1123(dateString); + } + + OffsetDateTime getDate() { + return (date == null) ? null : date.getDateTime(); + } + } + + public static final class InvalidStronglyTypedHeaders { + public InvalidStronglyTypedHeaders(HttpHeaders httpHeaders) throws Exception { + throw new Exception(); + } + } + + private static byte[] getObjectBytes(Object value) { + return String.valueOf(value).getBytes(StandardCharsets.UTF_8); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JsonSerializableEndToEndTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JsonSerializableEndToEndTests.java new file mode 100644 index 0000000000000..77397d247a8e1 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/JsonSerializableEndToEndTests.java @@ -0,0 +1,163 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import com.azure.json.JsonReader; +import com.azure.json.JsonSerializable; +import com.azure.json.JsonToken; +import com.azure.json.JsonWriter; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Tests that the Jackson Databind {@link com.fasterxml.jackson.databind.Module} is able to handle deserializing and + * serializing {@link JsonSerializable} within an {@link ObjectMapper} context. + */ +public class JsonSerializableEndToEndTests { + private static final SerializerAdapter ADAPTER = JacksonAdapter.createDefaultSerializerAdapter(); + + @Test + public void serialization() throws IOException { + JsonSerializableWrapper wrapper = new JsonSerializableWrapper() + .setGeneralProperties(new GeneralProperties(42, true, "hello world", -0.0D)); + String expected = "{\"jsonserializable\":{\"anInt\":42,\"aBoolean\":true,\"aString\":\"hello world\"," + + "\"aNullableDecimal\":-0.0}}"; + + String actual = ADAPTER.serialize(wrapper, SerializerEncoding.JSON); + assertEquals(expected, actual); + } + + @Test + public void deserialization() throws IOException { + String json = "{\"jsonserializable\":{\"anInt\":42,\"aBoolean\":true,\"aString\":\"hello world\"," + + "\"aNullableDecimal\":-0.0}}"; + JsonSerializableWrapper expected = new JsonSerializableWrapper() + .setGeneralProperties(new GeneralProperties(42, true, "hello world", -0.0D)); + + JsonSerializableWrapper actual = ADAPTER.deserialize(json, JsonSerializableWrapper.class, + SerializerEncoding.JSON); + assertEquals(expected, actual); + } + + /** + * Class that wraps a {@link JsonSerializable} type. + */ + public static final class JsonSerializableWrapper { + @JsonProperty("jsonserializable") + private GeneralProperties generalProperties; + + public JsonSerializableWrapper setGeneralProperties(GeneralProperties generalProperties) { + this.generalProperties = generalProperties; + return this; + } + + public GeneralProperties getGeneralProperties() { + return generalProperties; + } + + @Override + public int hashCode() { + return Objects.hashCode(generalProperties); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof JsonSerializableWrapper)) { + return false; + } + + return Objects.equals(generalProperties, ((JsonSerializableWrapper) obj).generalProperties); + } + } + + /** + * Class that implements {@link JsonSerializable}. + */ + public static final class GeneralProperties implements JsonSerializable { + private final int anInt; + private final boolean aBoolean; + private final String aString; + private final Double aNullableDecimal; + + public GeneralProperties(int anInt, boolean aBoolean, String aString, Double aNullableDecimal) { + this.anInt = anInt; + this.aBoolean = aBoolean; + this.aString = aString; + this.aNullableDecimal = aNullableDecimal; + } + + @Override + public JsonWriter toJson(JsonWriter jsonWriter) throws IOException { + return jsonWriter.writeStartObject() + .writeIntField("anInt", anInt) + .writeBooleanField("aBoolean", aBoolean) + .writeStringField("aString", aString) + .writeNumberField("aNullableDecimal", aNullableDecimal) + .writeEndObject(); + } + + /** + * Deserializes an instance of {@link GeneralProperties} from the {@link JsonReader}. + * + * @param jsonReader The {@link JsonReader} being read. + * @return An instance of {@link GeneralProperties}, or null if the {@link JsonReader} was pointing to JSON + * null. + * @throws IOException If an error occurs during deserialization. + */ + public static GeneralProperties fromJson(JsonReader jsonReader) throws IOException { + return jsonReader.readObject(reader -> { + int anInt = 0; + boolean aBoolean = false; + String aString = null; + Double aNullableDouble = null; + + while (reader.nextToken() != JsonToken.END_OBJECT) { + String fieldName = reader.getFieldName(); + reader.nextToken(); + + if ("anInt".equals(fieldName)) { + anInt = reader.getInt(); + } else if ("aBoolean".equals(fieldName)) { + aBoolean = reader.getBoolean(); + } else if ("aString".equals(fieldName)) { + aString = reader.getString(); + } else if ("aNullableDecimal".equals(fieldName)) { + aNullableDouble = reader.getNullable(JsonReader::getDouble); + } else { + reader.skipChildren(); + } + } + + return new GeneralProperties(anInt, aBoolean, aString, aNullableDouble); + }); + } + + @Override + public int hashCode() { + return Objects.hash(anInt, aBoolean, aString, aNullableDecimal); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GeneralProperties)) { + return false; + } + + GeneralProperties other = (GeneralProperties) obj; + return anInt == other.anInt + && aBoolean == other.aBoolean + && Objects.equals(aString, other.aString) + && Objects.equals(aNullableDecimal, other.aNullableDecimal); + } + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/ResponseErrorDeserializerTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/ResponseErrorDeserializerTests.java new file mode 100644 index 0000000000000..cf5ff7c5dbf4a --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/ResponseErrorDeserializerTests.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.models.ResponseError; +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for {@code ResponseErrorDeserializer}. + */ +public class ResponseErrorDeserializerTests { + private static final JacksonAdapter MAPPER = new JacksonAdapter(); + + @ParameterizedTest + @MethodSource("deserializeOffsetDateTimeSupplier") + public void deserializeJson(String responseWithError, String expectedCode, String expectedMessage) throws IOException { + ResponseError deserialize = MAPPER.deserialize(responseWithError, ResponseError.class, SerializerEncoding.JSON); + assertEquals(expectedCode, deserialize.getCode()); + assertEquals(expectedMessage, deserialize.getMessage()); + } + + @Test + public void deserializeResponseErrorMissingRequiredProperty() { + // code is a required property and exception should be thrown if it's missing + String missingCodeWithErrorWrapper = "{\"error\": {\"message\": \"Invalid syntax\"}}"; + Assertions.assertThrows(MismatchedInputException.class, () -> MAPPER.deserialize(missingCodeWithErrorWrapper, ResponseError.class, SerializerEncoding.JSON)); + + String missingCodeWithoutErrorWrapper = "{\"message\": \"Invalid syntax\"}"; + Assertions.assertThrows(MismatchedInputException.class, () -> MAPPER.deserialize(missingCodeWithoutErrorWrapper, ResponseError.class, SerializerEncoding.JSON)); + + // message is a required property and exception should be thrown if it's missing + String missingMessageWithErrorWrapper = "{\"error\": {\"code\": \"BAD_QUERY_FORMAT\"}}"; + Assertions.assertThrows(MismatchedInputException.class, () -> MAPPER.deserialize(missingMessageWithErrorWrapper, ResponseError.class, SerializerEncoding.JSON)); + + String missingMessageWithoutErrorWrapper = "{\"code\": \"BAD_QUERY_FORMAT\"}"; + Assertions.assertThrows(MismatchedInputException.class, () -> MAPPER.deserialize(missingMessageWithoutErrorWrapper, ResponseError.class, SerializerEncoding.JSON)); + } + + private static Stream deserializeOffsetDateTimeSupplier() { + + return Stream.of( + Arguments.of("{\"error\": {\"code\": \"BAD_QUERY_FORMAT\", \"message\": \"Invalid syntax\"}}", "BAD_QUERY_FORMAT", "Invalid syntax"), + Arguments.of("{\"code\": \"BAD_QUERY_FORMAT\", \"message\": \"Invalid syntax\"}", "BAD_QUERY_FORMAT", "Invalid syntax"), + Arguments.of("{\"name\": \"foo\", \"error\": {\"code\": \"BAD_QUERY_FORMAT\", \"message\": \"Invalid syntax\"}}", "BAD_QUERY_FORMAT", "Invalid syntax"), + Arguments.of("{\"name\": \"foo\", \"code\": \"BAD_QUERY_FORMAT\", \"message\": \"Invalid syntax\"}", "BAD_QUERY_FORMAT", "Invalid syntax") + ); + } + +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyTests.java new file mode 100644 index 0000000000000..376f8c3851eae --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyTests.java @@ -0,0 +1,443 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.annotation.BodyParam; +import com.azure.core.annotation.ExpectedResponses; +import com.azure.core.annotation.Get; +import com.azure.core.annotation.HeaderParam; +import com.azure.core.annotation.Host; +import com.azure.core.annotation.PathParam; +import com.azure.core.annotation.Post; +import com.azure.core.annotation.ServiceInterface; +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpMethod; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.rest.RequestOptions; +import com.azure.core.http.rest.Response; +import com.azure.core.http.rest.RestProxy; +import com.azure.core.http.rest.StreamResponse; +import com.azure.core.implementation.http.rest.RestProxyUtils; +import com.azure.core.implementation.util.BinaryDataContent; +import com.azure.core.implementation.util.BinaryDataHelper; +import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.util.BinaryData; +import com.azure.core.util.Context; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.ByteArrayInputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests {@link RestProxy}. + */ +public class RestProxyTests { + + @Host("https://azure.com") + @ServiceInterface(name = "myService") + interface TestInterface { + @Post("my/url/path") + @ExpectedResponses({200}) + Mono> testMethod( + @BodyParam("application/octet-stream") Flux request, + @HeaderParam("Content-Type") String contentType, + @HeaderParam("Content-Length") Long contentLength + ); + + @Post("my/url/path") + @ExpectedResponses({200}) + Mono> testMethod( + @BodyParam("application/octet-stream") BinaryData data, + @HeaderParam("Content-Type") String contentType, + @HeaderParam("Content-Length") Long contentLength + ); + + @Get("{nextLink}") + @ExpectedResponses({200}) + Mono> testListNext(@PathParam(value = "nextLink", encoded = true) String nextLink); + + @Get("my/url/path") + @ExpectedResponses({200}) + Mono testMethodReturnsMonoVoid(); + + @Get("my/url/path") + @ExpectedResponses({200}) + void testVoidMethod(); + + @Get("my/url/path") + @ExpectedResponses({200}) + Mono> testMethodReturnsMonoResponseVoid(); + + @Get("my/url/path") + @ExpectedResponses({200}) + Response testMethodReturnsResponseVoid(); + + @Get("my/url/path") + @ExpectedResponses({200}) + StreamResponse testDownload(); + + @Get("my/url/path") + @ExpectedResponses({200}) + Mono testDownloadAsync(); + } + + @Test + public void contentTypeHeaderPriorityOverBodyParamAnnotationTest() { + HttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + byte[] bytes = "hello".getBytes(); + Response response = testInterface.testMethod(Flux.just(ByteBuffer.wrap(bytes)), + "application/json", (long) bytes.length) + .block(); + assertEquals(200, response.getStatusCode()); + } + + @Test + public void streamResponseShouldHaveHttpResponseReferenceSync() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + StreamResponse streamResponse = testInterface.testDownload(); + streamResponse.close(); + // This indirectly tests that StreamResponse has HttpResponse reference + assertTrue(client.closeCalledOnResponse); + } + + @Test + public void streamResponseShouldHaveHttpResponseReferenceAsync() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + StepVerifier.create( + testInterface.testDownloadAsync() + .doOnNext(StreamResponse::close)) + .expectNextCount(1) + .verifyComplete(); + // This indirectly tests that StreamResponse has HttpResponse reference + assertTrue(client.closeCalledOnResponse); + } + + @ParameterizedTest + @MethodSource("knownLengthBinaryDataIsPassthroughArgumentProvider") + public void knownLengthBinaryDataIsPassthrough(BinaryData data, long contentLength) { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + Response response = testInterface.testMethod(data, "application/json", contentLength).block(); + assertEquals(200, response.getStatusCode()); + assertSame(data, client.getLastHttpRequest().getBodyAsBinaryData()); + } + + private static Stream knownLengthBinaryDataIsPassthroughArgumentProvider() throws Exception { + String string = "hello"; + byte[] bytes = string.getBytes(); + Path file = Files.createTempFile("knownLengthBinaryDataIsPassthroughArgumentProvider", null); + file.toFile().deleteOnExit(); + Files.write(file, bytes); + return Stream.of( + Arguments.of(Named.of("bytes", BinaryData.fromBytes(bytes)), bytes.length), + Arguments.of(Named.of("string", BinaryData.fromString(string)), bytes.length), + Arguments.of(Named.of("file", BinaryData.fromFile(file)), bytes.length), + Arguments.of(Named.of("serializable", BinaryData.fromObject(bytes)), + BinaryData.fromObject(bytes).getLength()) + ); + } + + @ParameterizedTest + @MethodSource("doesNotChangeBinaryDataContentTypeDataProvider") + public void doesNotChangeBinaryDataContentType(BinaryData data, long contentLength) { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + Class expectedContentClazz = BinaryDataHelper.getContent(data).getClass(); + + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + Response response = testInterface.testMethod(data, + "application/json", contentLength) + .block(); + assertEquals(200, response.getStatusCode()); + + Class actualContentClazz = BinaryDataHelper.getContent( + client.getLastHttpRequest().getBodyAsBinaryData()).getClass(); + assertEquals(expectedContentClazz, actualContentClazz); + } + + @Test + public void monoVoidReturningApiClosesResponse() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + StepVerifier.create(testInterface.testMethodReturnsMonoVoid()) + .verifyComplete(); + + assertTrue(client.closeCalledOnResponse); + } + + @Test + public void voidReturningApiClosesResponse() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + + testInterface.testVoidMethod(); + + assertTrue(client.closeCalledOnResponse); + } + + @Test + public void voidReturningApiIgnoresResponseBody() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + + testInterface.testVoidMethod(); + + assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent()); + assertTrue(client.lastContext.getData("azure-ignore-response-body").isPresent()); + assertTrue((boolean) client.lastContext.getData("azure-ignore-response-body").get()); + } + + @Test + public void monoVoidReturningApiIgnoresResponseBody() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + StepVerifier.create( + testInterface.testMethodReturnsMonoVoid()) + .verifyComplete(); + + assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent()); + assertTrue(client.lastContext.getData("azure-ignore-response-body").isPresent()); + assertTrue((boolean) client.lastContext.getData("azure-ignore-response-body").get()); + } + + @Test + public void monoResponseVoidReturningApiIgnoresResponseBody() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + StepVerifier.create( + testInterface.testMethodReturnsMonoResponseVoid()) + .expectNextCount(1) + .verifyComplete(); + + assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent()); + assertTrue(client.lastContext.getData("azure-ignore-response-body").isPresent()); + assertTrue((boolean) client.lastContext.getData("azure-ignore-response-body").get()); + } + + @Test + public void responseVoidReturningApiIgnoresResponseBody() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + testInterface.testMethodReturnsResponseVoid(); + + assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent()); + assertTrue(client.lastContext.getData("azure-ignore-response-body").isPresent()); + assertTrue((boolean) client.lastContext.getData("azure-ignore-response-body").get()); + } + + @Test + public void streamResponseDoesNotEagerlyReadsResponse() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + testInterface.testDownload(); + + assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent()); + } + + @Test + public void monoWithStreamResponseDoesNotEagerlyReadsResponse() { + LocalHttpClient client = new LocalHttpClient(); + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(client) + .build(); + + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + StepVerifier.create(testInterface.testDownloadAsync().doOnNext(StreamResponse::close)) + .expectNextCount(1) + .verifyComplete(); + + assertFalse(client.lastContext.getData("azure-eagerly-read-response").isPresent()); + } + + private static Stream doesNotChangeBinaryDataContentTypeDataProvider() throws Exception { + String string = "hello"; + byte[] bytes = string.getBytes(); + Path file = Files.createTempFile("doesNotChangeBinaryDataContentTypeDataProvider", null); + file.toFile().deleteOnExit(); + Files.write(file, bytes); + ByteArrayInputStream stream = new ByteArrayInputStream(bytes); + return Stream.of( + Arguments.of(Named.of("bytes", BinaryData.fromBytes(bytes)), bytes.length), + Arguments.of(Named.of("string", BinaryData.fromString(string)), bytes.length), + Arguments.of(Named.of("file", BinaryData.fromFile(file)), bytes.length), + Arguments.of(Named.of("stream", BinaryData.fromStream(stream, (long) bytes.length)), bytes.length), + Arguments.of(Named.of("eager flux with length", + BinaryData.fromFlux(Flux.just(ByteBuffer.wrap(bytes))).block()), bytes.length), + Arguments.of(Named.of("lazy flux", + BinaryData.fromFlux(Flux.just(ByteBuffer.wrap(bytes)), null, false).block()), bytes.length), + Arguments.of(Named.of("lazy flux with length", + BinaryData.fromFlux(Flux.just(ByteBuffer.wrap(bytes)), (long) bytes.length, false).block()), bytes.length), + Arguments.of(Named.of("serializable", BinaryData.fromObject(bytes)), + BinaryData.fromObject(bytes).getLength()) + ); + } + + private static final class LocalHttpClient implements HttpClient { + + private volatile HttpRequest lastHttpRequest; + private volatile Context lastContext; + private volatile boolean closeCalledOnResponse; + + @Override + public Mono send(HttpRequest request) { + return send(request, Context.NONE); + } + + @Override + public Mono send(HttpRequest request, Context context) { + lastHttpRequest = request; + lastContext = context; + boolean success = request.getUrl().getPath().equals("/my/url/path"); + if (request.getHttpMethod().equals(HttpMethod.POST)) { + success &= "application/json".equals(request.getHeaders().getValue(HttpHeaderName.CONTENT_TYPE)); + } else { + success &= request.getHttpMethod().equals(HttpMethod.GET); + } + + return Mono.just(new MockHttpResponse(request, success ? 200 : 400) { + @Override + public void close() { + closeCalledOnResponse = true; + super.close(); + } + }); + } + + public HttpRequest getLastHttpRequest() { + return lastHttpRequest; + } + } + + @ParameterizedTest + @MethodSource("mergeRequestOptionsContextSupplier") + public void mergeRequestOptionsContext(Context context, RequestOptions options, + Map expectedContextValues) { + Map actualContextValues = RestProxyUtils.mergeRequestOptionsContext(context, options).getValues(); + + assertEquals(expectedContextValues.size(), actualContextValues.size()); + for (Map.Entry expectedKvp : expectedContextValues.entrySet()) { + assertTrue(actualContextValues.containsKey(expectedKvp.getKey()), () -> + "Missing expected key '" + expectedKvp.getKey() + "'."); + assertEquals(expectedKvp.getValue(), actualContextValues.get(expectedKvp.getKey())); + } + } + + private static Stream mergeRequestOptionsContextSupplier() { + Map twoValuesMap = new HashMap<>(); + twoValuesMap.put("key", "value"); + twoValuesMap.put("key2", "value2"); + + return Stream.of( + // Cases where the RequestOptions or it's Context doesn't exist. + Arguments.of(Context.NONE, null, Collections.emptyMap()), + Arguments.of(Context.NONE, new RequestOptions(), Collections.emptyMap()), + Arguments.of(Context.NONE, new RequestOptions().setContext(Context.NONE), Collections.emptyMap()), + + // Case where the RequestOptions Context is merged into an empty Context. + Arguments.of(Context.NONE, new RequestOptions().setContext(new Context("key", "value")), + Collections.singletonMap("key", "value")), + + // Case where the RequestOptions Context is merged, without replacement, into an existing Context. + Arguments.of(new Context("key", "value"), new RequestOptions().setContext(new Context("key2", "value2")), + twoValuesMap), + + // Case where the RequestOptions Context is merged and overrides an existing Context. + Arguments.of(new Context("key", "value"), new RequestOptions().setContext(new Context("key", "value2")), + Collections.singletonMap("key", "value2")) + ); + } + + @Test + public void doesNotChangeEncodedPath() { + String nextLinkUrl = + "https://management.azure.com:443/subscriptions/000/resourceGroups/rg/providers/Microsoft.Compute/virtualMachineScaleSets/vmss1/virtualMachines?api-version=2021-11-01&$skiptoken=Mzk4YzFjMzMtM2IwMC00OWViLWI2NGYtNjg4ZTRmZGQ1Nzc2IS9TdWJzY3JpcHRpb25zL2VjMGFhNWY3LTllNzgtNDBjOS04NWNkLTUzNWM2MzA1YjM4MC9SZXNvdXJjZUdyb3Vwcy9SRy1XRUlEWFUtVk1TUy9WTVNjYWxlU2V0cy9WTVNTMS9WTXMvNzc="; + HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(request -> { + assertEquals(nextLinkUrl, request.getUrl().toString()); + return Mono.just(new MockHttpResponse(null, 200)); + }) + .build(); + + TestInterface testInterface = RestProxy.create(TestInterface.class, pipeline); + + StepVerifier.create(testInterface.testListNext(nextLinkUrl)) + .expectNextCount(1) + .verifyComplete(); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyXMLTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyXMLTests.java new file mode 100644 index 0000000000000..95526eb7456bb --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/RestProxyXMLTests.java @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.annotation.BodyParam; +import com.azure.core.annotation.Get; +import com.azure.core.annotation.Host; +import com.azure.core.annotation.Put; +import com.azure.core.annotation.ServiceInterface; +import com.azure.core.http.HttpClient; +import com.azure.core.http.HttpHeaderName; +import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.HttpPipelineBuilder; +import com.azure.core.http.HttpRequest; +import com.azure.core.http.HttpResponse; +import com.azure.core.http.rest.RestProxy; +import com.azure.core.test.http.MockHttpResponse; +import com.azure.core.util.FluxUtil; +import com.azure.core.util.serializer.JacksonAdapter; +import com.azure.core.util.serializer.SerializerAdapter; +import com.azure.core.util.serializer.SerializerEncoding; +import com.azure.core.version.tests.models.AccessPolicy; +import com.azure.core.version.tests.models.SignedIdentifierInner; +import com.azure.core.version.tests.models.SignedIdentifiersWrapper; +import com.azure.core.version.tests.models.Slideshow; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class RestProxyXMLTests { + private static final SerializerAdapter ADAPTER = JacksonAdapter.createDefaultSerializerAdapter(); + + private static final String GET_CONTAINERS_ACLS = String.join("\n", + "", + "", + "", + " ", + " MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=", + " ", + " 2009-09-28T08:49:37.0000000Z", + " 2009-09-29T08:49:37.0000000Z", + " rwd", + " ", + " ", + ""); + + private static final String GET_XML_WITH_ATTRIBUTES = String.join("\n", + "", + "", + "", + " ", + " Wake up to WonderWidgets!", + " ", + " ", + " Overview", + " Why WonderWidgets are great", + " ", + " Who buys WonderWidgets", + " ", + ""); + + static class MockXMLHTTPClient implements HttpClient { + private HttpResponse response(HttpRequest request, String xml) { + HttpHeaders headers = new HttpHeaders().set(HttpHeaderName.CONTENT_TYPE, "application/xml"); + return new MockHttpResponse(request, 200, headers, xml.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public Mono send(HttpRequest request) { + if (request.getUrl().toString().endsWith("GetContainerACLs")) { + return Mono.just(response(request, GET_CONTAINERS_ACLS)); + } else if (request.getUrl().toString().endsWith("GetXMLWithAttributes")) { + return Mono.just(response(request, GET_XML_WITH_ATTRIBUTES)); + } else { + return Mono.just(new MockHttpResponse(request, 404)); + } + } + } + + @Host("http://unused") + @ServiceInterface(name = "MyXMLService") + interface MyXMLService { + @Get("GetContainerACLs") + SignedIdentifiersWrapper getContainerACLs(); + + @Put("SetContainerACLs") + void setContainerACLs(@BodyParam("application/xml") SignedIdentifiersWrapper signedIdentifiers); + } + + @Test + public void canReadXMLResponse() { + // + final HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(new MockXMLHTTPClient()) + .build(); + + // + MyXMLService myXMLService = RestProxy.create(MyXMLService.class, pipeline, ADAPTER); + List identifiers = myXMLService.getContainerACLs().signedIdentifiers(); + assertNotNull(identifiers); + assertNotEquals(0, identifiers.size()); + } + + static class MockXMLReceiverClient implements HttpClient { + byte[] receivedBytes = null; + + @Override + public Mono send(HttpRequest request) { + if (request.getUrl().toString().endsWith("SetContainerACLs")) { + return FluxUtil.collectBytesInByteBufferStream(request.getBody()) + .map(bytes -> { + receivedBytes = bytes; + return new MockHttpResponse(request, 200); + }); + } else { + return Mono.just(new MockHttpResponse(request, 404)); + } + } + } + + @Test + public void canWriteXMLRequest() throws IOException { + SignedIdentifierInner si = new SignedIdentifierInner(); + si.withId("MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="); + + AccessPolicy ap = new AccessPolicy(); + ap.withStart(OffsetDateTime.parse("2009-09-28T08:49:37.0000000Z")); + ap.withExpiry(OffsetDateTime.parse("2009-09-29T08:49:37.0000000Z")); + ap.withPermission("rwd"); + + si.withAccessPolicy(ap); + List expectedAcls = Collections.singletonList(si); + + MockXMLReceiverClient httpClient = new MockXMLReceiverClient(); + // + final HttpPipeline pipeline = new HttpPipelineBuilder() + .httpClient(httpClient) + .build(); + // + MyXMLService myXMLService = RestProxy.create(MyXMLService.class, pipeline, ADAPTER); + SignedIdentifiersWrapper wrapper = new SignedIdentifiersWrapper(expectedAcls); + myXMLService.setContainerACLs(wrapper); + + SignedIdentifiersWrapper actualAclsWrapped = ADAPTER.deserialize( + new String(httpClient.receivedBytes, StandardCharsets.UTF_8), SignedIdentifiersWrapper.class, + SerializerEncoding.XML); + + List actualAcls = actualAclsWrapped.signedIdentifiers(); + + // Ideally we'd just check for "things that matter" about the XML-- e.g. the tag names, structure, and attributes needs to be the same, + // but it doesn't matter if one document has a trailing newline or has UTF-8 in the header instead of utf-8, or if comments are missing. + assertEquals(expectedAcls.size(), actualAcls.size()); + assertEquals(expectedAcls.get(0).id(), actualAcls.get(0).id()); + assertEquals(expectedAcls.get(0).accessPolicy().expiry(), actualAcls.get(0).accessPolicy().expiry()); + assertEquals(expectedAcls.get(0).accessPolicy().start(), actualAcls.get(0).accessPolicy().start()); + assertEquals(expectedAcls.get(0).accessPolicy().permission(), actualAcls.get(0).accessPolicy().permission()); + } + + @Host("http://unused") + @ServiceInterface(name = "MyXMLServiceWithAttributes") + public interface MyXMLServiceWithAttributes { + @Get("GetXMLWithAttributes") + Slideshow getSlideshow(); + } + + @Test + public void canDeserializeXMLWithAttributes() throws Exception { + // + final HttpPipeline pipeline = new HttpPipelineBuilder().httpClient(new MockXMLHTTPClient()).build(); + + // + MyXMLServiceWithAttributes myXMLService = RestProxy.create(MyXMLServiceWithAttributes.class, pipeline, ADAPTER); + + Slideshow slideshow = myXMLService.getSlideshow(); + assertEquals("Sample Slide Show", slideshow.title()); + assertEquals("Date of publication", slideshow.date()); + assertEquals("Yours Truly", slideshow.author()); + assertEquals(2, slideshow.slides().length); + + assertEquals("all", slideshow.slides()[0].type()); + assertEquals("Wake up to WonderWidgets!", slideshow.slides()[0].title()); + assertEquals(0, slideshow.slides()[0].items().length); + + assertEquals("all", slideshow.slides()[1].type()); + assertEquals("Overview", slideshow.slides()[1].title()); + assertEquals(3, slideshow.slides()[1].items().length); + assertEquals("Why WonderWidgets are great", slideshow.slides()[1].items()[0]); + assertEquals("", slideshow.slides()[1].items()[1]); + assertEquals("Who buys WonderWidgets", slideshow.slides()[1].items()[2]); + + String xml = ADAPTER.serialize(slideshow, SerializerEncoding.XML); + Slideshow newSlideshow = ADAPTER.deserialize(xml, Slideshow.class, SerializerEncoding.XML); + String newXML = ADAPTER.serialize(newSlideshow, SerializerEncoding.XML); + assertEquals(xml, newXML); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/SerializerEncodingTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/SerializerEncodingTests.java new file mode 100644 index 0000000000000..5a101bd1892f0 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/SerializerEncodingTests.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.core.version.tests; + +import com.azure.core.http.HttpHeaders; +import com.azure.core.util.serializer.SerializerEncoding; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.Collections; + +/** + * Tests for {@link SerializerEncoding}. + */ +class SerializerEncodingTests { + private static final String CONTENT_TYPE = "Content-Type"; + + @ParameterizedTest + @ValueSource(strings = { + "application/xml", "application/atom+xml", "text/xml", "application/foo+XML", "TEXT/XML", + "application/xml;charset=utf-8", "application/atom+xml; charset=utf-32" + }) + void recognizeXml(String mimeType) { + // Arrange + HttpHeaders headers = new HttpHeaders(Collections.singletonMap(CONTENT_TYPE, mimeType)); + + // Act & Assert + Assertions.assertEquals(SerializerEncoding.XML, SerializerEncoding.fromHeaders(headers)); + } + + @ParameterizedTest + @ValueSource(strings = { + "application/json", "application/kv+json", "APPLICATION/JSON", "application/FOO+JSON", + "application/json;charset=utf-8", "application/config+json; charset=utf-32" + }) + void recognizeJson(String mimeType) { + // Arrange + HttpHeaders headers = new HttpHeaders(Collections.singletonMap(CONTENT_TYPE, mimeType)); + + // Act & Assert + Assertions.assertEquals(SerializerEncoding.JSON, SerializerEncoding.fromHeaders(headers)); + } + + @ParameterizedTest + @ValueSource(strings = { + "text/css", "text/csv", "text/html", "text/javascript", "text/plain", "TEXT/PLAIN", "text/plain; charset=utf-8" + }) + void recognizeText(String mimeType) { + // Arrange + HttpHeaders headers = new HttpHeaders(Collections.singletonMap(CONTENT_TYPE, mimeType)); + + // Act & Assert + Assertions.assertEquals(SerializerEncoding.TEXT, SerializerEncoding.fromHeaders(headers)); + } + + @Test + void defaultNoContentType() { + // Arrange + HttpHeaders headers = new HttpHeaders(Collections.singletonMap("Http-Method", "GET")); + + // Act & Assert + Assertions.assertEquals(SerializerEncoding.JSON, SerializerEncoding.fromHeaders(headers)); + } + + @ParameterizedTest + @ValueSource(strings = {"application/binary", "invalid-mime-type"}) + void defaultUnsupportedType(String mimeType) { + // Arrange + HttpHeaders headers = new HttpHeaders(Collections.singletonMap(CONTENT_TYPE, mimeType)); + + // Act & Assert + Assertions.assertEquals(SerializerEncoding.JSON, SerializerEncoding.fromHeaders(headers)); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/TypeReferenceTests.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/TypeReferenceTests.java new file mode 100644 index 0000000000000..9299500e79126 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/TypeReferenceTests.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests; + +import com.azure.core.util.serializer.TypeReference; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TypeReferenceTests { + + @Test + public void createGenericTypeReference() { + final TypeReference> typeReference = new TypeReference>() { }; + final Map expectedJavaType = new HashMap() { }; + assertEquals(expectedJavaType.getClass().getGenericSuperclass(), typeReference.getJavaType()); + assertEquals(HashMap.class, typeReference.getJavaClass()); + } + + @Test + public void createFactoryInstance() { + TypeReference typeReference = TypeReference.createInstance(int.class); + assertEquals(int.class, typeReference.getJavaType()); + assertEquals(int.class, typeReference.getJavaClass()); + } + + @SuppressWarnings("rawtypes") + @Test + public void createTypeReferenceWithoutType() { + IllegalArgumentException thrown + = assertThrows(IllegalArgumentException.class, () -> new TypeReference() { }); + assertEquals("Type constructed without type information.", thrown.getMessage()); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AccessPolicy.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AccessPolicy.java new file mode 100644 index 0000000000000..5b151dd86cc6a --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AccessPolicy.java @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.OffsetDateTime; + +/** + * An Access policy. + */ +public class AccessPolicy { + /** + * the date-time the policy is active. + */ + @JsonProperty(value = "Start") + private OffsetDateTime start; + + /** + * the date-time the policy expires. + */ + @JsonProperty(value = "Expiry") + private OffsetDateTime expiry; + + /** + * the permissions for the acl policy. + */ + @JsonProperty(value = "Permission") + private String permission; + + /** + * Get the start value. + * + * @return the start value + */ + public OffsetDateTime start() { + return this.start; + } + + /** + * Set the start value. + * + * @param start the start value to set + * @return the AccessPolicy object itself. + */ + public AccessPolicy withStart(OffsetDateTime start) { + this.start = start; + return this; + } + + /** + * Get the expiry value. + * + * @return the expiry value + */ + public OffsetDateTime expiry() { + return this.expiry; + } + + /** + * Set the expiry value. + * + * @param expiry the expiry value to set + * @return the AccessPolicy object itself. + */ + public AccessPolicy withExpiry(OffsetDateTime expiry) { + this.expiry = expiry; + return this; + } + + /** + * Get the permission value. + * + * @return the permission value + */ + public String permission() { + return this.permission; + } + + /** + * Set the permission value. + * + * @param permission the permission value to set + * @return the AccessPolicy object itself. + */ + public AccessPolicy withPermission(String permission) { + this.permission = permission; + return this; + } + +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AnimalShelter.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AnimalShelter.java new file mode 100644 index 0000000000000..ba69d1ccdf0aa --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AnimalShelter.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonFlatten +public class AnimalShelter { + @JsonProperty(value = "properties.description") + private String description; + + @JsonProperty(value = "properties.animalsInfo", required = true) + private List animalsInfo; + + public String description() { + return this.description; + } + + public AnimalShelter withDescription(String description) { + this.description = description; + return this; + } + + public List animalsInfo() { + return this.animalsInfo; + } + + public AnimalShelter withAnimalsInfo(List animalsInfo) { + this.animalsInfo = animalsInfo; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AnimalWithTypeIdContainingDot.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AnimalWithTypeIdContainingDot.java new file mode 100644 index 0000000000000..060dba84da752 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/AnimalWithTypeIdContainingDot.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "@odata\\.type", + defaultImpl = AnimalWithTypeIdContainingDot.class) +@JsonTypeName("AnimalWithTypeIdContainingDot") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "#Favourite.Pet.DogWithTypeIdContainingDot", + value = DogWithTypeIdContainingDot.class), + @JsonSubTypes.Type(name = "#Favourite.Pet.CatWithTypeIdContainingDot", + value = CatWithTypeIdContainingDot.class), + @JsonSubTypes.Type(name = "#Favourite.Pet.RabbitWithTypeIdContainingDot", + value = RabbitWithTypeIdContainingDot.class) +}) +public class AnimalWithTypeIdContainingDot { +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/CatWithTypeIdContainingDot.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/CatWithTypeIdContainingDot.java new file mode 100644 index 0000000000000..c7f6ff0ba91af --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/CatWithTypeIdContainingDot.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "@odata\\.type", + defaultImpl = CatWithTypeIdContainingDot.class) +@JsonTypeName("#Favourite.Pet.CatWithTypeIdContainingDot") +public class CatWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { + @JsonProperty(value = "breed", required = true) + private String breed; + + public String breed() { + return this.breed; + } + + public CatWithTypeIdContainingDot withBreed(String presetName) { + this.breed = presetName; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/ClassWithFlattenedProperties.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/ClassWithFlattenedProperties.java new file mode 100644 index 0000000000000..dcfdd7a7c344a --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/ClassWithFlattenedProperties.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Immutable; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Immutable +public final class ClassWithFlattenedProperties { + @JsonFlatten + @JsonProperty(value = "@odata.type") + private final String odataType; + + @JsonProperty(value = "@odata.etag") + private final String odataETag; + + @JsonCreator + public ClassWithFlattenedProperties(@JsonProperty(value = "@odata.type") String odataType, + @JsonProperty(value = "@odata.etag") String odataETag) { + this.odataType = odataType; + this.odataETag = odataETag; + } + + public String getOdataType() { + return odataType; + } + + public String getOdataETag() { + return odataETag; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/ComposeTurtles.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/ComposeTurtles.java new file mode 100644 index 0000000000000..9ae94d86be45d --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/ComposeTurtles.java @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class ComposeTurtles { + @JsonProperty(value = "description") + private String description; + + @JsonProperty(value = "turtlesSet1Lead") + private TurtleWithTypeIdContainingDot turtlesSet1Lead; + + @JsonProperty(value = "turtlesSet1") + private List turtlesSet1; + + @JsonProperty(value = "turtlesSet2Lead") + private NonEmptyAnimalWithTypeIdContainingDot turtlesSet2Lead; + + @JsonProperty(value = "turtlesSet2") + private List turtlesSet2; + + public String description() { + return this.description; + } + + public ComposeTurtles withDescription(String description) { + this.description = description; + return this; + } + + public List turtlesSet1() { + return this.turtlesSet1; + } + + public TurtleWithTypeIdContainingDot turtlesSet1Lead() { + return this.turtlesSet1Lead; + } + + public ComposeTurtles withTurtlesSet1Lead(TurtleWithTypeIdContainingDot lead) { + this.turtlesSet1Lead = lead; + return this; + } + + public ComposeTurtles withTurtlesSet1(List turtles) { + this.turtlesSet1 = turtles; + return this; + } + + public List turtlesSet2() { + return this.turtlesSet2; + } + + public NonEmptyAnimalWithTypeIdContainingDot turtlesSet2Lead() { + return this.turtlesSet2Lead; + } + + public ComposeTurtles withTurtlesSet2Lead(NonEmptyAnimalWithTypeIdContainingDot lead) { + this.turtlesSet2Lead = lead; + return this; + } + + public ComposeTurtles withTurtlesSet2(List turtles) { + this.turtlesSet2 = turtles; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/DogWithTypeIdContainingDot.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/DogWithTypeIdContainingDot.java new file mode 100644 index 0000000000000..af6d03bde608e --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/DogWithTypeIdContainingDot.java @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "@odata\\.type", + defaultImpl = DogWithTypeIdContainingDot.class) +@JsonTypeName("#Favourite.Pet.DogWithTypeIdContainingDot") +public class DogWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { + @JsonProperty(value = "breed") + private String breed; + + // Flattenable property + @JsonProperty(value = "properties.cuteLevel") + private Integer cuteLevel; + + public String breed() { + return this.breed; + } + + public DogWithTypeIdContainingDot withBreed(String audioLanguage) { + this.breed = audioLanguage; + return this; + } + + public Integer cuteLevel() { + return this.cuteLevel; + } + + public DogWithTypeIdContainingDot withCuteLevel(Integer cuteLevel) { + this.cuteLevel = cuteLevel; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenDangling.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenDangling.java new file mode 100644 index 0000000000000..8758edea67d28 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenDangling.java @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Class for testing serialization. + */ +public class FlattenDangling { + @JsonProperty("a.flattened.property") + @JsonFlatten + private String flattenedProperty; + + public String getFlattenedProperty() { + return flattenedProperty; + } + + public FlattenDangling setFlattenedProperty(String flattenedProperty) { + this.flattenedProperty = flattenedProperty; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenableAnimalInfo.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenableAnimalInfo.java new file mode 100644 index 0000000000000..9ce0a2633f80a --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenableAnimalInfo.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class FlattenableAnimalInfo { + @JsonProperty(value = "home") + private String home; + + @JsonProperty(value = "animal", required = true) + private AnimalWithTypeIdContainingDot animal; + + public String home() { + return this.home; + } + + public FlattenableAnimalInfo withHome(String home) { + this.home = home; + return this; + } + + public AnimalWithTypeIdContainingDot animal() { + return this.animal; + } + + public FlattenableAnimalInfo withAnimal(AnimalWithTypeIdContainingDot animal) { + this.animal = animal; + return this; + } + +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenedProduct.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenedProduct.java new file mode 100644 index 0000000000000..b2c4d8bfcef61 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenedProduct.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +@JsonFlatten +public class FlattenedProduct { + // Flattened and escaped property + @JsonProperty(value = "properties.p\\.name") + private String productName; + + @JsonProperty(value = "properties.type") + private String productType; + + public String getProductName() { + return this.productName; + } + + public FlattenedProduct setProductName(String productName) { + this.productName = productName; + return this; + } + + public String getProductType() { + return this.productType; + } + + public FlattenedProduct setProductType(String productType) { + this.productType = productType; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenedPropertiesAndJsonAnyGetter.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenedPropertiesAndJsonAnyGetter.java new file mode 100644 index 0000000000000..66798619b0e13 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FlattenedPropertiesAndJsonAnyGetter.java @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class with {@link JsonFlatten} on a property along with {@link JsonAnyGetter} on a method. + */ +@Fluent +public final class FlattenedPropertiesAndJsonAnyGetter { + @JsonFlatten + @JsonProperty("flattened.string") + private String string; + + @JsonIgnore + private Map additionalProperties; + + public FlattenedPropertiesAndJsonAnyGetter setString(String string) { + this.string = string; + return this; + } + + public String getString() { + return string; + } + + @JsonAnyGetter + public Map additionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public FlattenedPropertiesAndJsonAnyGetter addAdditionalProperty(String key, Object value) { + if (additionalProperties == null) { + additionalProperties = new HashMap<>(); + } + + additionalProperties.put(key, value); + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Foo.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Foo.java new file mode 100644 index 0000000000000..39ea98f18ca0c --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Foo.java @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.List; +import java.util.Map; + +/** + * Class for testing serialization. + */ +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "$type") +@JsonTypeName("foo") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "foochild", value = FooChild.class) +}) +public class Foo { + @JsonProperty(value = "properties.bar") + private String bar; + @JsonProperty(value = "properties.props.baz") + private List baz; + @JsonProperty(value = "properties.props.q.qux") + private Map qux; + @JsonProperty(value = "properties.more\\.props") + private String moreProps; + @JsonProperty(value = "props.empty") + private Integer empty; + @JsonProperty(value = "") + private Map additionalProperties; + + public String bar() { + return bar; + } + + public void bar(String bar) { + this.bar = bar; + } + + public List baz() { + return baz; + } + + public void baz(List baz) { + this.baz = baz; + } + + public Map qux() { + return qux; + } + + public void qux(Map qux) { + this.qux = qux; + } + + public String moreProps() { + return moreProps; + } + + public void moreProps(String moreProps) { + this.moreProps = moreProps; + } + + public Integer empty() { + return empty; + } + + public void empty(Integer empty) { + this.empty = empty; + } + + public Map additionalProperties() { + return additionalProperties; + } + + public void additionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FooChild.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FooChild.java new file mode 100644 index 0000000000000..4c21e4704f5e4 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/FooChild.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +/** + * Class for testing serialization. + */ +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "$type") +@JsonTypeName("foochild") +public class FooChild extends Foo { +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/HttpBinJSON.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/HttpBinJSON.java new file mode 100644 index 0000000000000..daf896c5927fc --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/HttpBinJSON.java @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Maps to the JSON return values from http://httpbin.org. + */ +public class HttpBinJSON { + @JsonProperty() + private String url; + + @JsonProperty() + private Map headers; + + @JsonProperty() + private Object data; + + /** + * Gets the URL associated with this request. + * + * @return he URL associated with the request. + */ + public String url() { + return url; + } + + /** + * Sets the URL associated with this request. + * + * @param url The URL associated with the request. + */ + public void url(String url) { + this.url = url; + } + + /** + * Gets the response headers. + * + * @return The response headers. + */ + public Map headers() { + return headers; + } + + /** + * Sets the response headers. + * + * @param headers The response headers. + */ + public void headers(Map headers) { + this.headers = headers; + } + + /** + * Gets the response body. + * + * @return The response body. + */ + public Object data() { + return data; + } + + /** + * Sets the response body. + * + * @param data The response body. + */ + public void data(Object data) { + this.data = data; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenNestedInner.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenNestedInner.java new file mode 100644 index 0000000000000..54a81d1df47b3 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenNestedInner.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +@JsonFlatten +public class JsonFlattenNestedInner { + + @JsonFlatten + @JsonProperty(value = "identity") + private VirtualMachineIdentity identity; + + public VirtualMachineIdentity getIdentity() { + return identity; + } + + public JsonFlattenNestedInner setIdentity(VirtualMachineIdentity identity) { + this.identity = identity; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnArrayType.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnArrayType.java new file mode 100644 index 0000000000000..2495b3e0e334f --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnArrayType.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.azure.core.util.CoreUtils; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class JsonFlattenOnArrayType { + @JsonFlatten + @JsonProperty("jsonflatten.array") + private String[] jsonFlattenArray; + + public JsonFlattenOnArrayType setJsonFlattenArray(String[] jsonFlattenArray) { + this.jsonFlattenArray = CoreUtils.clone(jsonFlattenArray); + return this; + } + + public String[] getJsonFlattenArray() { + return CoreUtils.clone(jsonFlattenArray); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnCollectionType.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnCollectionType.java new file mode 100644 index 0000000000000..3908442ac092b --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnCollectionType.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class JsonFlattenOnCollectionType { + @JsonFlatten + @JsonProperty("jsonflatten.collection") + private List jsonFlattenCollection; + + public JsonFlattenOnCollectionType setJsonFlattenCollection(List jsonFlattenCollection) { + this.jsonFlattenCollection = jsonFlattenCollection; + return this; + } + + public List getJsonFlattenCollection() { + return jsonFlattenCollection; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnJsonIgnoredProperty.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnJsonIgnoredProperty.java new file mode 100644 index 0000000000000..5f12859fbccc7 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnJsonIgnoredProperty.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class JsonFlattenOnJsonIgnoredProperty { + @JsonProperty("name") + private String name; + + @JsonIgnore + @JsonFlatten + @JsonProperty("jsonflatten.ignored") + private String ignored; + + public JsonFlattenOnJsonIgnoredProperty setName(String name) { + this.name = name; + return this; + } + + public String getName() { + return name; + } + + public JsonFlattenOnJsonIgnoredProperty setIgnored(String ignored) { + this.ignored = ignored; + return this; + } + + public String getIgnored() { + return ignored; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnPrimitiveType.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnPrimitiveType.java new file mode 100644 index 0000000000000..68d6f5c344e00 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenOnPrimitiveType.java @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class JsonFlattenOnPrimitiveType { + @JsonFlatten + @JsonProperty("jsonflatten.boolean") + private boolean jsonFlattenBoolean; + + @JsonFlatten + @JsonProperty("jsonflatten.decimal") + private double jsonFlattenDecimal; + + @JsonFlatten + @JsonProperty("jsonflatten.number") + private int jsonFlattenNumber; + + @JsonFlatten + @JsonProperty("jsonflatten.string") + private String jsonFlattenString; + + public JsonFlattenOnPrimitiveType setJsonFlattenBoolean(boolean jsonFlattenBoolean) { + this.jsonFlattenBoolean = jsonFlattenBoolean; + return this; + } + + public boolean isJsonFlattenBoolean() { + return jsonFlattenBoolean; + } + + public JsonFlattenOnPrimitiveType setJsonFlattenDecimal(double jsonFlattenDecimal) { + this.jsonFlattenDecimal = jsonFlattenDecimal; + return this; + } + + public double getJsonFlattenDecimal() { + return jsonFlattenDecimal; + } + + public JsonFlattenOnPrimitiveType setJsonFlattenNumber(int jsonFlattenNumber) { + this.jsonFlattenNumber = jsonFlattenNumber; + return this; + } + + public int getJsonFlattenNumber() { + return jsonFlattenNumber; + } + + public JsonFlattenOnPrimitiveType setJsonFlattenString(String jsonFlattenString) { + this.jsonFlattenString = jsonFlattenString; + return this; + } + + public String getJsonFlattenString() { + return jsonFlattenString; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenWithJsonInfoDiscriminator.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenWithJsonInfoDiscriminator.java new file mode 100644 index 0000000000000..a470479e5ee32 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/JsonFlattenWithJsonInfoDiscriminator.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = JsonFlattenWithJsonInfoDiscriminator.class) +@JsonTypeName("JsonFlattenWithJsonInfoDiscriminator") +@Fluent +public final class JsonFlattenWithJsonInfoDiscriminator { + @JsonFlatten + @JsonProperty("jsonflatten.discriminator") + private String jsonFlattenDiscriminator; + + public JsonFlattenWithJsonInfoDiscriminator setJsonFlattenDiscriminator(String jsonFlattenDiscriminator) { + this.jsonFlattenDiscriminator = jsonFlattenDiscriminator; + return this; + } + + public String getJsonFlattenDiscriminator() { + return jsonFlattenDiscriminator; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NewFoo.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NewFoo.java new file mode 100644 index 0000000000000..5510534582a2b --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NewFoo.java @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class for testing serialization. + */ +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "$type") +@JsonTypeName("newfoo") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "newfoochild", value = NewFooChild.class) +}) +public class NewFoo { + @JsonProperty(value = "properties.bar") + private String bar; + @JsonProperty(value = "properties.props.baz") + private List baz; + @JsonProperty(value = "properties.props.q.qux") + private Map qux; + @JsonProperty(value = "properties.more\\.props") + private String moreProps; + @JsonProperty(value = "props.empty") + private Integer empty; + @JsonIgnore + private Map additionalProperties; + @JsonProperty(value = "additionalProperties") + private Map additionalPropertiesProperty; + + public String bar() { + return bar; + } + + public void bar(String bar) { + this.bar = bar; + } + + public List baz() { + return baz; + } + + public void baz(List baz) { + this.baz = baz; + } + + public Map qux() { + return qux; + } + + public void qux(Map qux) { + this.qux = qux; + } + + public String moreProps() { + return moreProps; + } + + public void moreProps(String moreProps) { + this.moreProps = moreProps; + } + + public Integer empty() { + return empty; + } + + public void empty(Integer empty) { + this.empty = empty; + } + + @JsonAnySetter + private void additionalProperties(String key, Object value) { + if (additionalProperties == null) { + additionalProperties = new HashMap<>(); + } + additionalProperties.put(key.replace("\\.", "."), value); + } + + @JsonAnyGetter + public Map additionalProperties() { + return additionalProperties; + } + + public void additionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + } + + public Map additionalPropertiesProperty() { + return additionalPropertiesProperty; + } + + public void additionalPropertiesProperty(Map additionalPropertiesProperty) { + this.additionalPropertiesProperty = additionalPropertiesProperty; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NewFooChild.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NewFooChild.java new file mode 100644 index 0000000000000..41dc68927dd20 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NewFooChild.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +/** + * Class for testing serialization. + */ +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "$type") +@JsonTypeName("newfoochild") +public class NewFooChild extends NewFoo { +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NonEmptyAnimalWithTypeIdContainingDot.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NonEmptyAnimalWithTypeIdContainingDot.java new file mode 100644 index 0000000000000..8425bcd44131e --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/NonEmptyAnimalWithTypeIdContainingDot.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, + property = "@odata\\.type", + defaultImpl = NonEmptyAnimalWithTypeIdContainingDot.class) +@JsonTypeName("NonEmptyAnimalWithTypeIdContainingDot") +@JsonSubTypes({ + @JsonSubTypes.Type(name = "#Favourite.Pet.TurtleWithTypeIdContainingDot", + value = TurtleWithTypeIdContainingDot.class) +}) +public class NonEmptyAnimalWithTypeIdContainingDot { + @JsonProperty(value = "age") + private Integer age; + + public Integer age() { + return this.age; + } + + public NonEmptyAnimalWithTypeIdContainingDot withAge(Integer age) { + this.age = age; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/RabbitWithTypeIdContainingDot.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/RabbitWithTypeIdContainingDot.java new file mode 100644 index 0000000000000..a732829cd6e54 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/RabbitWithTypeIdContainingDot.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +import java.util.List; + +@JsonFlatten +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "@odata\\.type", + defaultImpl = RabbitWithTypeIdContainingDot.class) +@JsonTypeName("#Favourite.Pet.RabbitWithTypeIdContainingDot") +public class RabbitWithTypeIdContainingDot extends AnimalWithTypeIdContainingDot { + @JsonProperty(value = "tailLength") + private Integer tailLength; + + @JsonProperty(value = "meals") + private List meals; + + public Integer filters() { + return this.tailLength; + } + + public RabbitWithTypeIdContainingDot withTailLength(Integer tailLength) { + this.tailLength = tailLength; + return this; + } + + public List meals() { + return this.meals; + } + + public RabbitWithTypeIdContainingDot withMeals(List meals) { + this.meals = meals; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SampleResource.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SampleResource.java new file mode 100644 index 0000000000000..2b8a72a8d1091 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SampleResource.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +@JsonFlatten +public class SampleResource { + + @JsonProperty(value = "properties.name") + private String namePropertiesName; + + @JsonProperty(value = "properties.registrationTtl") + private String registrationTtl; + + public SampleResource withNamePropertiesName(String namePropertiesName) { + this.namePropertiesName = namePropertiesName; + return this; + } + + public SampleResource withRegistrationTtl(String registrationTtl) { + this.registrationTtl = registrationTtl; + return this; + } + + public String getNamePropertiesName() { + return namePropertiesName; + } + + public String getRegistrationTtl() { + return registrationTtl; + } +} + diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/School.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/School.java new file mode 100644 index 0000000000000..55909447c95bd --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/School.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@SuppressWarnings({"unused", "FieldCanBeLocal"}) +@JsonFlatten +public class School { + @JsonProperty(value = "teacher") + private Teacher teacher; + + @JsonProperty(value = "properties.name") + private String name; + + @JsonProperty(value = "tags") + private Map tags; + + public School setTeacher(Teacher teacher) { + this.teacher = teacher; + return this; + } + + public School setName(String name) { + this.name = name; + return this; + } + + public School setTags(Map tags) { + this.tags = tags; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SignedIdentifierInner.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SignedIdentifierInner.java new file mode 100644 index 0000000000000..a86a1f11baa71 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SignedIdentifierInner.java @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * signed identifier. + */ +public class SignedIdentifierInner { + /** + * a unique id. + */ + @JsonProperty(value = "Id", required = true) + private String id; + + /** + * The access policy. + */ + @JsonProperty(value = "AccessPolicy", required = true) + private AccessPolicy accessPolicy; + + /** + * Get the id value. + * + * @return the id value + */ + public String id() { + return this.id; + } + + /** + * Set the id value. + * + * @param id the id value to set + * @return the SignedIdentifierInner object itself. + */ + public SignedIdentifierInner withId(String id) { + this.id = id; + return this; + } + + /** + * Get the accessPolicy value. + * + * @return the accessPolicy value + */ + public AccessPolicy accessPolicy() { + return this.accessPolicy; + } + + /** + * Set the accessPolicy value. + * + * @param accessPolicy the accessPolicy value to set + * @return the SignedIdentifierInner object itself. + */ + public SignedIdentifierInner withAccessPolicy(AccessPolicy accessPolicy) { + this.accessPolicy = accessPolicy; + return this; + } + +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SignedIdentifiersWrapper.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SignedIdentifiersWrapper.java new file mode 100644 index 0000000000000..9614ae0406f50 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/SignedIdentifiersWrapper.java @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +import java.util.List; + +@JacksonXmlRootElement(localName = "SignedIdentifiers") +public class SignedIdentifiersWrapper { + @JacksonXmlProperty(localName = "SignedIdentifier") + private final List signedIdentifiers; + + /** + * Creates a wrapper for {@code signedIdentifiers}. + * + * @param signedIdentifiers Identifiers to wrap. + */ + @JsonCreator + public SignedIdentifiersWrapper(@JsonProperty("signedIdentifiers") List signedIdentifiers) { + this.signedIdentifiers = signedIdentifiers; + } + + /** + * Get the SignedIdentifiers value. + * + * @return the SignedIdentifiers value + */ + public List signedIdentifiers() { + return signedIdentifiers; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Slide.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Slide.java new file mode 100644 index 0000000000000..59d86b2d0dc46 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Slide.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; + +import java.util.Arrays; + +public class Slide { + @JacksonXmlProperty(localName = "type", isAttribute = true) + private String type; + + @JsonProperty("title") + private String title; + + @JsonProperty("item") + private String[] items; + + /** + * Gets the type of slide. + * + * @return The type of slide. + */ + public String type() { + return type; + } + + /** + * Gets the slide title. + * + * @return The title of the slide. + */ + public String title() { + return title; + } + + /** + * Gets the content strings of the slide. + * + * @return The content strings of the slide. + */ + public String[] items() { + if (items == null) { + return new String[0]; + } + return Arrays.copyOf(items, items.length); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Slideshow.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Slideshow.java new file mode 100644 index 0000000000000..91d748603d382 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Slideshow.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; + +import java.util.Arrays; + +public class Slideshow { + @JacksonXmlProperty(localName = "title", isAttribute = true) + private String title; + + @JacksonXmlProperty(localName = "date", isAttribute = true) + private String date; + + @JacksonXmlProperty(localName = "author", isAttribute = true) + private String author; + + @JsonProperty("slide") + private Slide[] slides; + + /** + * Gets the title of the slideshow. + * + * @return The title of the slideshow. + */ + public String title() { + return title; + } + + /** + * Gets the date of publication. + * + * @return The date of publication. + */ + public String date() { + return date; + } + + /** + * Gets the slideshow author. + * + * @return Author of the slideshow. + */ + public String author() { + return author; + } + + /** + * Gets the slides in the slideshow. + * + * @return The slides in the slideshow. + */ + public Slide[] slides() { + if (slides == null) { + return new Slide[0]; + } + return Arrays.copyOf(slides, slides.length); + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Student.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Student.java new file mode 100644 index 0000000000000..0b450cd98605d --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Student.java @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; + +/** + * Model used for testing {@link JsonFlatten}. + */ +public class Student { +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Teacher.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Teacher.java new file mode 100644 index 0000000000000..e28bbbf728007 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/Teacher.java @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Map; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@SuppressWarnings({"FieldCanBeLocal", "unused"}) +public class Teacher { + @JsonProperty(value = "students") + private Map students; + + public Teacher setStudents(Map students) { + this.students = students; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/TurtleWithTypeIdContainingDot.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/TurtleWithTypeIdContainingDot.java new file mode 100644 index 0000000000000..c5c69b1779ffa --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/TurtleWithTypeIdContainingDot.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "@odata\\.type", + defaultImpl = TurtleWithTypeIdContainingDot.class) +@JsonTypeName("#Favourite.Pet.TurtleWithTypeIdContainingDot") +public class TurtleWithTypeIdContainingDot extends NonEmptyAnimalWithTypeIdContainingDot { + @JsonProperty(value = "size") + private Integer size; + + public Integer size() { + return this.size; + } + + public TurtleWithTypeIdContainingDot withSize(Integer size) { + this.size = size; + return this; + } +} + diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineIdentity.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineIdentity.java new file mode 100644 index 0000000000000..5312f71de94e6 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineIdentity.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class VirtualMachineIdentity { + + @JsonProperty(value = "type") + private List type; + + @JsonProperty(value = "userAssignedIdentities") + private Map userAssignedIdentities; + + public List getType() { + return type; + } + + public VirtualMachineIdentity setType(List type) { + this.type = type; + return this; + } + + public Map getUserAssignedIdentities() { + return userAssignedIdentities; + } + + public VirtualMachineIdentity setUserAssignedIdentities( + Map userAssignedIdentities) { + this.userAssignedIdentities = userAssignedIdentities; + return this; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSet.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSet.java new file mode 100644 index 0000000000000..521cf680b0261 --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSet.java @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class VirtualMachineScaleSet { + @JsonFlatten + @JsonProperty(value = "properties.virtualMachineProfile") + private VirtualMachineScaleSetVMProfile virtualMachineProfile; + + public VirtualMachineScaleSet setVirtualMachineProfile(VirtualMachineScaleSetVMProfile virtualMachineProfile) { + this.virtualMachineProfile = virtualMachineProfile; + return this; + } + + public VirtualMachineScaleSetVMProfile getVirtualMachineProfile() { + return virtualMachineProfile; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetNetworkConfiguration.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetNetworkConfiguration.java new file mode 100644 index 0000000000000..a3ae5194ab5be --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetNetworkConfiguration.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class VirtualMachineScaleSetNetworkConfiguration { + @JsonProperty(value = "name") + private String name; + + @JsonFlatten + @JsonProperty(value = "properties.primary") + private Boolean primary; + + public VirtualMachineScaleSetNetworkConfiguration setName(String name) { + this.name = name; + return this; + } + + public String getName() { + return name; + } + + public VirtualMachineScaleSetNetworkConfiguration setPrimary(Boolean primary) { + this.primary = primary; + return this; + } + + public Boolean getPrimary() { + return primary; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetNetworkProfile.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetNetworkProfile.java new file mode 100644 index 0000000000000..f95da7023de7d --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetNetworkProfile.java @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class VirtualMachineScaleSetNetworkProfile { + @JsonProperty(value = "networkInterfaceConfigurations") + private List networkInterfaceConfigurations; + + public VirtualMachineScaleSetNetworkProfile setNetworkInterfaceConfigurations( + List networkInterfaceConfigurations) { + this.networkInterfaceConfigurations = networkInterfaceConfigurations; + return this; + } + + public List getNetworkInterfaceConfigurations() { + return networkInterfaceConfigurations; + } +} diff --git a/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetVMProfile.java b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetVMProfile.java new file mode 100644 index 0000000000000..dba0431310c1b --- /dev/null +++ b/sdk/core/azure-core-version-tests/src/test/java/com/azure/core/version/tests/models/VirtualMachineScaleSetVMProfile.java @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.version.tests.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.JsonFlatten; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Model used for testing {@link JsonFlatten}. + */ +@Fluent +public final class VirtualMachineScaleSetVMProfile { + @JsonProperty(value = "networkProfile") + private VirtualMachineScaleSetNetworkProfile networkProfile; + + public VirtualMachineScaleSetVMProfile setNetworkProfile(VirtualMachineScaleSetNetworkProfile networkProfile) { + this.networkProfile = networkProfile; + return this; + } + + public VirtualMachineScaleSetNetworkProfile getNetworkProfile() { + return networkProfile; + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/JacksonDatabind212.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/JacksonDatabind212.java index b72ad21c2a737..2ecb5ae971b26 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/JacksonDatabind212.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/JacksonDatabind212.java @@ -3,20 +3,27 @@ package com.azure.core.implementation.jackson; -import com.azure.core.util.logging.ClientLogger; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer; import com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy; import com.fasterxml.jackson.databind.introspect.AnnotatedClass; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.util.AccessPattern; /** * Utility methods for Jackson Databind types when it's known that the version is 2.12+. */ final class JacksonDatabind212 { - private static final ClientLogger LOGGER = new ClientLogger(JacksonDatabind212.class); /** * Mutates the passed {@link ObjectMapper} to coerce empty strings as null. @@ -27,7 +34,38 @@ final class JacksonDatabind212 { * @return The updated {@link ObjectMapper}. */ static ObjectMapper mutateXmlCoercions(ObjectMapper mapper) { - mapper.coercionConfigDefaults().setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); + // https://github.com/FasterXML/jackson-dataformat-xml/pull/585/files fixed array and collection elements + // with coercion to be handled by the coercion config below which is a backwards compatibility breaking + // change for us. Handle empty string items within an array or collection as empty string. + mapper.registerModule(new SimpleModule().setDeserializerModifier(new BeanDeserializerModifier() { + @Override + public JsonDeserializer modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, + JsonDeserializer deserializer) { + if (String.class.isAssignableFrom(beanDesc.getBeanClass())) { + return new DelegatingDeserializer(deserializer) { + @Override + protected JsonDeserializer newDelegatingInstance(JsonDeserializer newDelegatee) { + return this; + } + + @Override + public AccessPattern getNullAccessPattern() { + return AccessPattern.DYNAMIC; + } + + @Override + public Object getNullValue(DeserializationContext ctxt) throws JsonMappingException { + return (ctxt.getParser().getParsingContext().inArray()) ? "" : super.getNullValue(ctxt); + } + }; + } else { + return deserializer; + } + } + })); + + mapper.coercionConfigDefaults() + .setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull); return mapper; }