diff --git a/README.md b/README.md index ec17d63d..5d93e8b7 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,26 @@ The recommended way to use the AWS Glue Schema Registry Library for Java is to c ``` +### Jackson Support for Java 8 Date Types in JSON + +To support Java 8 Dates in JSON add the [Jackson JSR310 Datatype](https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310) dependency to implementing project class path. + +For example, in a Maven based project include the latest dependency. + +```xml + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.16.1 + +``` + +Then add the following configuration property to specify the fully qualified class path to the JavaTimeModule like so. + +```java +properties.put(AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE, "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"); +``` + #### Producer for Kafka with PROTOBUF format ```java diff --git a/common/src/main/java/com/amazonaws/services/schemaregistry/common/configs/GlueSchemaRegistryConfiguration.java b/common/src/main/java/com/amazonaws/services/schemaregistry/common/configs/GlueSchemaRegistryConfiguration.java index ce7d870d..af582b1c 100644 --- a/common/src/main/java/com/amazonaws/services/schemaregistry/common/configs/GlueSchemaRegistryConfiguration.java +++ b/common/src/main/java/com/amazonaws/services/schemaregistry/common/configs/GlueSchemaRegistryConfiguration.java @@ -22,6 +22,7 @@ import com.amazonaws.services.schemaregistry.utils.ProtobufMessageType; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.module.SimpleModule; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.EnumUtils; @@ -58,6 +59,8 @@ public class GlueSchemaRegistryConfiguration { private Map metadata; private String secondaryDeserializer; private URI proxyUrl; + private String javaTimeModuleClass; + private String objectMapperFactory = "com.amazonaws.services.schemaregistry.utils.json.DefaultObjectMapperFactory"; /** * Name of the application using the serializer/deserializer. @@ -104,6 +107,8 @@ private void buildSchemaRegistryConfigs(Map configs) { validateAndSetUserAgent(configs); validateAndSetSecondaryDeserializer(configs); validateAndSetProxyUrl(configs); + validateAndSetJavaTimeModule(configs); + validateAndSetObjectMapperFactory(configs); } private void validateAndSetSecondaryDeserializer(Map configs) { @@ -323,6 +328,29 @@ private void validateAndSetJacksonDeserializationFeatures(Map configs } } + private void validateAndSetJavaTimeModule(Map configs) { + if (isPresent(configs, AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE)) { + String moduleClassName = String.valueOf(configs.get(AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE)); + this.javaTimeModuleClass = moduleClassName; + } + } + + private void validateAndSetObjectMapperFactory(Map configs) { + if (isPresent(configs, AWSSchemaRegistryConstants.OBJECT_MAPPER_FACTORY)) { + this.objectMapperFactory = String.valueOf(configs.get(AWSSchemaRegistryConstants.OBJECT_MAPPER_FACTORY)); + } + } + + public SimpleModule loadJavaTimeModule() { + try { + Class moduleClass = Class.forName(this.getJavaTimeModuleClass()); + return (SimpleModule) moduleClass.getConstructor().newInstance(); + } catch (Exception e) { + String message = String.format("Invalid JavaTimeModule specified: %s", this.javaTimeModuleClass); + throw new AWSSchemaRegistryException(message, e); + } + } + private boolean isPresent(Map configs, String key) { if (!GlueSchemaRegistryUtils.getInstance() diff --git a/common/src/main/java/com/amazonaws/services/schemaregistry/utils/AWSSchemaRegistryConstants.java b/common/src/main/java/com/amazonaws/services/schemaregistry/utils/AWSSchemaRegistryConstants.java index 5ce30b06..3c63018a 100644 --- a/common/src/main/java/com/amazonaws/services/schemaregistry/utils/AWSSchemaRegistryConstants.java +++ b/common/src/main/java/com/amazonaws/services/schemaregistry/utils/AWSSchemaRegistryConstants.java @@ -172,6 +172,19 @@ public final class AWSSchemaRegistryConstants { */ public static final String USER_AGENT_APP = "userAgentApp"; + /** + * Jackson Java Time Module to use for handling Java 8 Date/Time. + * By default, no module is registered. + * Ex: com.fasterxml.jackson.datatype.jsr310.JavaTimeModule + */ + public static final String REGISTER_JAVA_TIME_MODULE = "registerJavaTimeModule"; + + /** + * Factory for creating custom Jackson ObjectMapper instances for use in handling JSON. + * Default: com.amazonaws.services.schemaregistry.utils.json.DefaultObjectMapperFactory + */ + public static final String OBJECT_MAPPER_FACTORY = "objectMapperFactory"; + /** * Private constructor to avoid initialization of the class. */ diff --git a/common/src/test/java/com/amazonaws/services/schemaregistry/common/AWSSchemaRegistryClientTest.java b/common/src/test/java/com/amazonaws/services/schemaregistry/common/AWSSchemaRegistryClientTest.java index c6a221ae..4c06c896 100644 --- a/common/src/test/java/com/amazonaws/services/schemaregistry/common/AWSSchemaRegistryClientTest.java +++ b/common/src/test/java/com/amazonaws/services/schemaregistry/common/AWSSchemaRegistryClientTest.java @@ -32,25 +32,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.services.glue.GlueClient; -import software.amazon.awssdk.services.glue.model.CreateSchemaRequest; -import software.amazon.awssdk.services.glue.model.CreateSchemaResponse; -import software.amazon.awssdk.services.glue.model.DataFormat; -import software.amazon.awssdk.services.glue.model.EntityNotFoundException; -import software.amazon.awssdk.services.glue.model.GetSchemaByDefinitionRequest; -import software.amazon.awssdk.services.glue.model.GetSchemaByDefinitionResponse; -import software.amazon.awssdk.services.glue.model.GetSchemaVersionRequest; -import software.amazon.awssdk.services.glue.model.GetSchemaVersionResponse; -import software.amazon.awssdk.services.glue.model.GetTagsRequest; -import software.amazon.awssdk.services.glue.model.GetTagsResponse; -import software.amazon.awssdk.services.glue.model.MetadataKeyValuePair; -import software.amazon.awssdk.services.glue.model.PutSchemaVersionMetadataRequest; -import software.amazon.awssdk.services.glue.model.PutSchemaVersionMetadataResponse; -import software.amazon.awssdk.services.glue.model.QuerySchemaVersionMetadataRequest; -import software.amazon.awssdk.services.glue.model.QuerySchemaVersionMetadataResponse; -import software.amazon.awssdk.services.glue.model.RegisterSchemaVersionRequest; -import software.amazon.awssdk.services.glue.model.RegisterSchemaVersionResponse; -import software.amazon.awssdk.services.glue.model.RegistryId; -import software.amazon.awssdk.services.glue.model.SchemaId; +import software.amazon.awssdk.services.glue.model.*; import java.io.File; import java.io.IOException; @@ -68,10 +50,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class AWSSchemaRegistryClientTest { @@ -693,6 +672,31 @@ public void testQuerySchemaTags_clientThrowsException_throwsException() throws N assertEquals(expectedMsg, awsSchemaRegistryException.getMessage()); } + @Test + public void testCreateSchema_existingSchema_throwsAlreadyExistsException() throws NoSuchFieldException, IllegalAccessException { + String testSchemaName = "test-schema"; + String testSchemaDefinition = "test-schema-definition"; + awsSchemaRegistryClient = configureAWSSchemaRegistryClientWithSerdeConfig(awsSchemaRegistryClient, + glueSchemaRegistryConfiguration); + + AWSSchemaRegistryClient awsSchemaRegistryClientSpy = spy(awsSchemaRegistryClient); + AlreadyExistsException ex = AlreadyExistsException.builder().message("Already exists").build(); + when(mockGlueClient.createSchema(any(CreateSchemaRequest.class))).thenThrow(ex); + + UUID expectedId = UUID.randomUUID(); + String expectedDataFormat = DataFormat.AVRO.toString(); + Map expectedMetadata = getMetadata(); + doReturn(expectedId).when(awsSchemaRegistryClientSpy).registerSchemaVersion( + testSchemaDefinition, + testSchemaName, + expectedDataFormat, + expectedMetadata + ); + + UUID actualId = awsSchemaRegistryClientSpy.createSchema(testSchemaName, expectedDataFormat, testSchemaDefinition, expectedMetadata); + assertEquals(expectedId, actualId); + } + private Map getMetadata() { Map metadata = new HashMap<>(); metadata.put("event-source-1", "topic1"); diff --git a/common/src/test/java/com/amazonaws/services/schemaregistry/common/configs/GlueSchemaRegistryConfigurationTest.java b/common/src/test/java/com/amazonaws/services/schemaregistry/common/configs/GlueSchemaRegistryConfigurationTest.java index 249c4605..471e67a4 100644 --- a/common/src/test/java/com/amazonaws/services/schemaregistry/common/configs/GlueSchemaRegistryConfigurationTest.java +++ b/common/src/test/java/com/amazonaws/services/schemaregistry/common/configs/GlueSchemaRegistryConfigurationTest.java @@ -18,6 +18,8 @@ import com.amazonaws.services.schemaregistry.exception.AWSSchemaRegistryException; import com.amazonaws.services.schemaregistry.utils.AWSSchemaRegistryConstants; import com.amazonaws.services.schemaregistry.utils.AvroRecordType; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.SerializationFeature; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,11 +34,7 @@ import java.util.Map; import java.util.Properties; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; /** * Unit tests for testing configuration elements. @@ -410,4 +408,55 @@ public void testBuildConfig_invalidProxyUrl_throwsException() { Exception exception = assertThrows(AWSSchemaRegistryException.class, () -> new GlueSchemaRegistryConfiguration(props)); assertEquals("Proxy URL property is not a valid URL: "+proxy, exception.getMessage()); } + + @Test + public void testBuildConfig_javaTimeModuleEnabledMock_succeeds() { + Properties props = createTestProperties(); + String moduleClassName = "com.amazonaws.services.schemaregistry.common.configs.TestableSimpleModule"; + props.put(AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE, moduleClassName); + GlueSchemaRegistryConfiguration cfg = new GlueSchemaRegistryConfiguration(props); + assertNotNull(cfg.loadJavaTimeModule()); + } + + @Test + public void testBuildConfig_javaTimeModuleEnabledWithoutDependency_throwException() { + Properties props = createTestProperties(); + String moduleClassName = "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"; + props.put(AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE, moduleClassName); + GlueSchemaRegistryConfiguration cfg = new GlueSchemaRegistryConfiguration(props); + Exception exception = assertThrows(AWSSchemaRegistryException.class, () -> cfg.loadJavaTimeModule()); + String message = String.format("Invalid JavaTimeModule specified: %s", moduleClassName); + assertEquals(message, exception.getMessage()); + } + + @Test + public void testBuildConfig_specifyObjectMapperFactory_succeeds() { + Properties props = createTestProperties(); + String factoryClass = "com.amazonaws.services.schemaregistry.common.configs.TestableSimpleModule"; // could be any string + props.put(AWSSchemaRegistryConstants.OBJECT_MAPPER_FACTORY, factoryClass); + GlueSchemaRegistryConfiguration cfg = new GlueSchemaRegistryConfiguration(props); + assertEquals(factoryClass, cfg.getObjectMapperFactory()); + } + + @Test + public void testBuildConfig_jacksonSerializationFeatures_succeeds() { + Properties props = createTestProperties(); + List features = new ArrayList<>(); + features.add(SerializationFeature.INDENT_OUTPUT.toString()); + features.add(SerializationFeature.WRAP_EXCEPTIONS.toString()); + props.put(AWSSchemaRegistryConstants.JACKSON_SERIALIZATION_FEATURES, features); + GlueSchemaRegistryConfiguration cfg = new GlueSchemaRegistryConfiguration(props); + assertEquals(cfg.getJacksonSerializationFeatures().size(), 2); + } + + @Test + public void testBuildConfig_jacksonDeserializationFeatures_succeeds() { + Properties props = createTestProperties(); + List features = new ArrayList<>(); + features.add(DeserializationFeature.WRAP_EXCEPTIONS.toString()); + features.add(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES.toString()); + props.put(AWSSchemaRegistryConstants.JACKSON_DESERIALIZATION_FEATURES, features); + GlueSchemaRegistryConfiguration cfg = new GlueSchemaRegistryConfiguration(props); + assertEquals(cfg.getJacksonDeserializationFeatures().size(), 2); + } } \ No newline at end of file diff --git a/common/src/test/java/com/amazonaws/services/schemaregistry/common/configs/TestableSimpleModule.java b/common/src/test/java/com/amazonaws/services/schemaregistry/common/configs/TestableSimpleModule.java new file mode 100644 index 00000000..fbc68016 --- /dev/null +++ b/common/src/test/java/com/amazonaws/services/schemaregistry/common/configs/TestableSimpleModule.java @@ -0,0 +1,6 @@ +package com.amazonaws.services.schemaregistry.common.configs; + +import com.fasterxml.jackson.databind.module.SimpleModule; + +public class TestableSimpleModule extends SimpleModule { +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index c7e76867..87849321 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -176,6 +176,10 @@ org.hamcrest hamcrest-all + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + javax.xml.bind jaxb-api diff --git a/integration-tests/src/test/java/com/amazonaws/services/schemaregistry/integrationtests/kafka/ObjectMapperDateTimeSupportTest.java b/integration-tests/src/test/java/com/amazonaws/services/schemaregistry/integrationtests/kafka/ObjectMapperDateTimeSupportTest.java new file mode 100644 index 00000000..bf37405f --- /dev/null +++ b/integration-tests/src/test/java/com/amazonaws/services/schemaregistry/integrationtests/kafka/ObjectMapperDateTimeSupportTest.java @@ -0,0 +1,71 @@ +package com.amazonaws.services.schemaregistry.integrationtests.kafka; + +import com.amazonaws.services.schemaregistry.common.configs.GlueSchemaRegistryConfiguration; +import com.amazonaws.services.schemaregistry.utils.AWSSchemaRegistryConstants; +import com.amazonaws.services.schemaregistry.utils.json.ObjectMapperUtils; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class ObjectMapperDateTimeSupportTest { + + @Test + void testCreate_useJavaTimeModule_succeeds() { + Map map = new HashMap<>(); + map.put(AWSSchemaRegistryConstants.AWS_REGION, "US-West-1"); + map.put(AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE, "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"); + GlueSchemaRegistryConfiguration cfg = new GlueSchemaRegistryConfiguration(map); + + ObjectMapper om = ObjectMapperUtils.create(cfg); + assertNotNull(cfg.loadJavaTimeModule()); + assertNotNull(om); + } + + @Test + void testCreate_noJavaTimeModule_failsSerializingJava8DateTime() throws JsonProcessingException { + Map map = new HashMap<>(); + map.put(AWSSchemaRegistryConstants.AWS_REGION, "US-West-1"); + GlueSchemaRegistryConfiguration cfg = new GlueSchemaRegistryConfiguration(map); + + ObjectMapper om = ObjectMapperUtils.create(cfg); + + MyTestObject myObj = new MyTestObject("obj1", LocalDateTime.now()); + + assertThrows(InvalidDefinitionException.class, () -> om.writeValueAsString(myObj)); + } + + @Test + void testCreate_withJavaTimeModule_succeedsSerializingAndDeserializingJava8DateTime() throws JsonProcessingException { + Map map = new HashMap<>(); + map.put(AWSSchemaRegistryConstants.AWS_REGION, "US-West-1"); + map.put(AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE, "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"); + GlueSchemaRegistryConfiguration cfg = new GlueSchemaRegistryConfiguration(map); + + ObjectMapper om = ObjectMapperUtils.create(cfg); + + MyTestObject expectedObj = new MyTestObject("obj1", LocalDateTime.now()); + String jsonStr = om.writeValueAsString(expectedObj); + + MyTestObject actualObj = om.readValue(jsonStr, MyTestObject.class); + assertNotNull(actualObj); + assertEquals(expectedObj.getCreated(), actualObj.getCreated()); + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + static class MyTestObject { + String name; + LocalDateTime created; + } +} diff --git a/integration-tests/src/test/java/com/amazonaws/services/schemaregistry/integrationtests/kinesis/GlueSchemaRegistryKinesisIntegrationTest.java b/integration-tests/src/test/java/com/amazonaws/services/schemaregistry/integrationtests/kinesis/GlueSchemaRegistryKinesisIntegrationTest.java index 68a03098..be77c330 100644 --- a/integration-tests/src/test/java/com/amazonaws/services/schemaregistry/integrationtests/kinesis/GlueSchemaRegistryKinesisIntegrationTest.java +++ b/integration-tests/src/test/java/com/amazonaws/services/schemaregistry/integrationtests/kinesis/GlueSchemaRegistryKinesisIntegrationTest.java @@ -496,7 +496,7 @@ private String produceRecordsWithKPL(String streamName, byte[] serializedBytes = dataFormatSerializer.serialize(record); putFutures.add(producer.addUserRecord(streamName, Long.toString(timestamp.toEpochMilli()), null, - ByteBuffer.wrap(serializedBytes), gsrSchema)); + ByteBuffer.wrap(serializedBytes), null, gsrSchema)); } String shardId = null; diff --git a/jsonschema-kafkaconnect-converter/src/test/java/com/amazonaws/services/schemaregistry/kafkaconnect/jsonschema/FromConnectTest.java b/jsonschema-kafkaconnect-converter/src/test/java/com/amazonaws/services/schemaregistry/kafkaconnect/jsonschema/FromConnectTest.java index d3782e1b..959537eb 100644 --- a/jsonschema-kafkaconnect-converter/src/test/java/com/amazonaws/services/schemaregistry/kafkaconnect/jsonschema/FromConnectTest.java +++ b/jsonschema-kafkaconnect-converter/src/test/java/com/amazonaws/services/schemaregistry/kafkaconnect/jsonschema/FromConnectTest.java @@ -57,7 +57,7 @@ public class FromConnectTest { private static final JsonNodeFactory JSON_NODE_FACTORY = TypeConverter.JSON_NODE_FACTORY; private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - private static final JsonValidator JSON_VALIDATOR = new JsonValidator(); + private static final JsonValidator JSON_VALIDATOR = new JsonValidator(OBJECT_MAPPER); private ConnectSchemaToJsonSchemaConverter connectSchemaToJsonSchemaConverter; private ConnectValueToJsonNodeConverter connectValueToJsonNodeConverter; diff --git a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/deserializers/json/JsonDeserializer.java b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/deserializers/json/JsonDeserializer.java index 5c0c60a5..3a8231e4 100644 --- a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/deserializers/json/JsonDeserializer.java +++ b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/deserializers/json/JsonDeserializer.java @@ -20,16 +20,15 @@ import com.amazonaws.services.schemaregistry.exception.AWSSchemaRegistryException; import com.amazonaws.services.schemaregistry.serializers.json.JsonDataWithSchema; import com.amazonaws.services.schemaregistry.common.Schema; +import com.amazonaws.services.schemaregistry.utils.json.ObjectMapperUtils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import lombok.Builder; import lombok.Data; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.CollectionUtils; import java.io.IOException; import java.nio.ByteBuffer; @@ -56,19 +55,7 @@ public class JsonDeserializer implements GlueSchemaRegistryDataFormatDeserialize @Builder public JsonDeserializer(GlueSchemaRegistryConfiguration configs) { this.schemaRegistrySerDeConfigs = configs; - JsonNodeFactory jsonNodeFactory = JsonNodeFactory.withExactBigDecimals(true); - this.objectMapper = new ObjectMapper(); - this.objectMapper.setNodeFactory(jsonNodeFactory); - if (configs != null) { - if (!CollectionUtils.isEmpty(configs.getJacksonSerializationFeatures())) { - configs.getJacksonSerializationFeatures() - .forEach(this.objectMapper::enable); - } - if (!CollectionUtils.isEmpty(configs.getJacksonDeserializationFeatures())) { - configs.getJacksonDeserializationFeatures() - .forEach(this.objectMapper::enable); - } - } + this.objectMapper = ObjectMapperUtils.create(configs); } /** diff --git a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/serializers/json/JsonSerializer.java b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/serializers/json/JsonSerializer.java index 063bbf11..e1bccdc3 100644 --- a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/serializers/json/JsonSerializer.java +++ b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/serializers/json/JsonSerializer.java @@ -17,17 +17,16 @@ import com.amazonaws.services.schemaregistry.common.GlueSchemaRegistryDataFormatSerializer; import com.amazonaws.services.schemaregistry.common.configs.GlueSchemaRegistryConfiguration; import com.amazonaws.services.schemaregistry.exception.AWSSchemaRegistryException; +import com.amazonaws.services.schemaregistry.utils.json.ObjectMapperUtils; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.kjetland.jackson.jsonSchema.JsonSchemaGenerator; import lombok.Builder; import lombok.Getter; import lombok.NonNull; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.collections4.CollectionUtils; import java.nio.charset.StandardCharsets; @@ -36,7 +35,7 @@ */ @Slf4j public class JsonSerializer implements GlueSchemaRegistryDataFormatSerializer { - private static final JsonValidator JSON_VALIDATOR = new JsonValidator(); + private final JsonValidator jsonValidator; private final JsonSchemaGenerator jsonSchemaGenerator; private final ObjectMapper objectMapper; @Getter @@ -51,19 +50,8 @@ public class JsonSerializer implements GlueSchemaRegistryDataFormatSerializer { @Builder public JsonSerializer(GlueSchemaRegistryConfiguration configs) { this.schemaRegistrySerDeConfigs = configs; - JsonNodeFactory jsonNodeFactory = JsonNodeFactory.withExactBigDecimals(true); - this.objectMapper = new ObjectMapper(); - this.objectMapper.setNodeFactory(jsonNodeFactory); - if (configs != null) { - if (!CollectionUtils.isEmpty(configs.getJacksonSerializationFeatures())) { - configs.getJacksonSerializationFeatures() - .forEach(this.objectMapper::enable); - } - if (!CollectionUtils.isEmpty(configs.getJacksonDeserializationFeatures())) { - configs.getJacksonDeserializationFeatures() - .forEach(this.objectMapper::enable); - } - } + this.objectMapper = ObjectMapperUtils.create(configs); + this.jsonValidator = new JsonValidator(this.objectMapper); this.jsonSchemaGenerator = new JsonSchemaGenerator(this.objectMapper); } @@ -80,7 +68,7 @@ public byte[] serialize(Object data) { final JsonNode dataNode = getDataNode(data); final JsonNode schemaNode = getSchemaNode(data); - JSON_VALIDATOR.validateDataWithSchema(schemaNode, dataNode); + jsonValidator.validateDataWithSchema(schemaNode, dataNode); bytes = writeBytes(dataNode); return bytes; @@ -182,6 +170,6 @@ public void validate(String schemaDefinition, byte[] data) { public void validate(Object jsonDataWithSchema) { JsonNode schemaNode = getSchemaNode(jsonDataWithSchema); JsonNode dataNode = getDataNode(jsonDataWithSchema); - JSON_VALIDATOR.validateDataWithSchema(schemaNode, dataNode); + jsonValidator.validateDataWithSchema(schemaNode, dataNode); } } diff --git a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/serializers/json/JsonValidator.java b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/serializers/json/JsonValidator.java index 8b59c907..80dfbea9 100644 --- a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/serializers/json/JsonValidator.java +++ b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/serializers/json/JsonValidator.java @@ -30,6 +30,13 @@ * Json validator */ public class JsonValidator { + + private final ObjectMapper mapper; + + public JsonValidator(ObjectMapper mapper) { + this.mapper = mapper; + } + /** * Validates data against JsonSchema * @param schemaNode @@ -37,7 +44,7 @@ public class JsonValidator { */ public void validateDataWithSchema(JsonNode schemaNode, JsonNode dataNode) { try { - ObjectMapper mapper = new ObjectMapper(); +// ObjectMapper mapper = new ObjectMapper(); JSONObject rawSchema = new JSONObject(mapper.writeValueAsString(schemaNode)); Schema schema = SchemaLoader.load(rawSchema, new ReferenceDisabledSchemaClient()); diff --git a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/DefaultObjectMapperFactory.java b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/DefaultObjectMapperFactory.java new file mode 100644 index 00000000..c36b9a97 --- /dev/null +++ b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/DefaultObjectMapperFactory.java @@ -0,0 +1,33 @@ +package com.amazonaws.services.schemaregistry.utils.json; + +import com.amazonaws.services.schemaregistry.common.configs.GlueSchemaRegistryConfiguration; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import org.apache.commons.collections4.CollectionUtils; + +public class DefaultObjectMapperFactory implements ObjectMapperFactory { + + @Override + public ObjectMapper newInstance(GlueSchemaRegistryConfiguration configs) { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setNodeFactory(JsonNodeFactory.withExactBigDecimals(true)); + + if (configs != null) { + if (!CollectionUtils.isEmpty(configs.getJacksonSerializationFeatures())) { + configs.getJacksonSerializationFeatures() + .forEach(objectMapper::enable); + } + if (!CollectionUtils.isEmpty(configs.getJacksonDeserializationFeatures())) { + configs.getJacksonDeserializationFeatures() + .forEach(objectMapper::enable); + } + if (configs.getJavaTimeModuleClass() != null) { + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.registerModule(configs.loadJavaTimeModule()); + } + } + + return objectMapper; + } +} diff --git a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperFactory.java b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperFactory.java new file mode 100644 index 00000000..9e5b597a --- /dev/null +++ b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperFactory.java @@ -0,0 +1,9 @@ +package com.amazonaws.services.schemaregistry.utils.json; + +import com.amazonaws.services.schemaregistry.common.configs.GlueSchemaRegistryConfiguration; +import com.fasterxml.jackson.databind.ObjectMapper; + +public interface ObjectMapperFactory { + + ObjectMapper newInstance(GlueSchemaRegistryConfiguration configs); +} diff --git a/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperUtils.java b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperUtils.java new file mode 100644 index 00000000..2628b7d9 --- /dev/null +++ b/serializer-deserializer/src/main/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperUtils.java @@ -0,0 +1,20 @@ +package com.amazonaws.services.schemaregistry.utils.json; + +import com.amazonaws.services.schemaregistry.common.configs.GlueSchemaRegistryConfiguration; +import com.amazonaws.services.schemaregistry.exception.AWSSchemaRegistryException; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class ObjectMapperUtils { + + public static ObjectMapper create(GlueSchemaRegistryConfiguration configs) { + try { + Class moduleClass = Class.forName(configs.getObjectMapperFactory()); + ObjectMapperFactory factory = (ObjectMapperFactory) moduleClass.getConstructor().newInstance(); + return factory.newInstance(configs); + } catch (Exception e) { + String message = String.format("Failed to instantiate ObjectMapperFactory: %s", + configs.getObjectMapperFactory()); + throw new AWSSchemaRegistryException(message, e); + } + } +} diff --git a/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/deserializers/json/JsonDeserializerTest.java b/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/deserializers/json/JsonDeserializerTest.java index 6a7d63b4..411e132b 100644 --- a/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/deserializers/json/JsonDeserializerTest.java +++ b/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/deserializers/json/JsonDeserializerTest.java @@ -14,6 +14,7 @@ */ package com.amazonaws.services.schemaregistry.deserializers.json; +import com.amazonaws.services.schemaregistry.common.configs.GlueSchemaRegistryConfiguration; import org.junit.jupiter.api.Test; import java.nio.ByteBuffer; @@ -26,10 +27,10 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class JsonDeserializerTest { - private JsonDeserializer jsonDeserializer = new JsonDeserializer(null); @Test public void testDeserialize_nullArgs_throwsException() { + JsonDeserializer defaultJsonDeserializer = new JsonDeserializer(new GlueSchemaRegistryConfiguration("us-west-1")); String testSchemaDefinition = "{\"$id\":\"https://example.com/geographical-location.schema.json\"," + "\"$schema\":\"http://json-schema.org/draft-07/schema#\",\"title\":\"Longitude " + "and Latitude Values\",\"description\":\"A geographical coordinate.\"," @@ -42,8 +43,10 @@ public void testDeserialize_nullArgs_throwsException() { Schema testSchema = new Schema(testSchemaDefinition, DataFormat.JSON.name(), "testJson"); - assertThrows(IllegalArgumentException.class, () -> jsonDeserializer.deserialize(null, testSchema)); - assertThrows(IllegalArgumentException.class, () -> jsonDeserializer.deserialize(ByteBuffer.wrap(testBytes), + assertThrows(IllegalArgumentException.class, () -> defaultJsonDeserializer.deserialize(null, testSchema)); + assertThrows(IllegalArgumentException.class, () -> defaultJsonDeserializer.deserialize(ByteBuffer.wrap(testBytes), null)); } + + } diff --git a/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/serializers/json/JsonValidatorTest.java b/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/serializers/json/JsonValidatorTest.java index 4a686a9a..5774ead3 100644 --- a/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/serializers/json/JsonValidatorTest.java +++ b/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/serializers/json/JsonValidatorTest.java @@ -13,8 +13,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class JsonValidatorTest { - private JsonValidator validator = new JsonValidator(); private ObjectMapper mapper = new ObjectMapper(); + private JsonValidator validator = new JsonValidator(mapper); + private String stringSchema = "{\n" + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n" + " \"description\": \"String schema\",\n" diff --git a/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperUtilsTest.java b/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperUtilsTest.java new file mode 100644 index 00000000..96e47fec --- /dev/null +++ b/serializer-deserializer/src/test/java/com/amazonaws/services/schemaregistry/utils/json/ObjectMapperUtilsTest.java @@ -0,0 +1,61 @@ +package com.amazonaws.services.schemaregistry.utils.json; + +import com.amazonaws.services.schemaregistry.common.configs.GlueSchemaRegistryConfiguration; +import com.amazonaws.services.schemaregistry.exception.AWSSchemaRegistryException; +import com.amazonaws.services.schemaregistry.utils.AWSSchemaRegistryConstants; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +/** + * Unit tests for testing object mapper + */ +public class ObjectMapperUtilsTest { + + static GlueSchemaRegistryConfiguration testConfigs(Map map) { + return new GlueSchemaRegistryConfiguration(testMap(map)); + } + + static Map testMap(Map map) { + if (map == null) { + map = new HashMap<>(); + } + map.putIfAbsent(AWSSchemaRegistryConstants.AWS_REGION, "US-West-1"); + return map; + } + + @Test + void testCreate_succeeds() { + GlueSchemaRegistryConfiguration cfg = testConfigs(null); + + ObjectMapper om = ObjectMapperUtils.create(cfg); + assertNotNull(om); + } + + @Test + void testCreate_useJavaTimeModuleWithoutDeps_throwsException() { + Map map = testMap(null); + map.put(AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE, "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"); + GlueSchemaRegistryConfiguration cfg = testConfigs(map); + assertThrows(AWSSchemaRegistryException.class, () -> ObjectMapperUtils.create(cfg)); + } + + @Test + void testCreate_useJavaTimeModuleWithMockTimeModule_succeeds() { + Map map = testMap(null); + map.put(AWSSchemaRegistryConstants.REGISTER_JAVA_TIME_MODULE, "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"); + + GlueSchemaRegistryConfiguration spy = spy(testConfigs(map)); + doReturn(new SimpleModule()).when(spy).loadJavaTimeModule(); + + ObjectMapper om = ObjectMapperUtils.create(spy); + assertNotNull(om); + } +}