From e68f02aa2fcf6486b300cb3089c5f64f7f7c22c2 Mon Sep 17 00:00:00 2001 From: Liudmila Molkova Date: Tue, 21 Sep 2021 10:26:38 -0700 Subject: [PATCH] Use ObjectMapperShim in all core packages, deprecate direct ObjectMapper usage (#24138) * Use ObjectMapperShim in all core packages, deprecate direct ObjectMapper usage --- .../checkstyle/checkstyle-suppressions.xml | 4 + .../resources/spotbugs/spotbugs-exclude.xml | 7 + .../azure-verticals-agrifood-farming/pom.xml | 3 + .../azure-security-confidentialledger/pom.xml | 3 + .../serializer/AzureJacksonAdapter.java | 3 +- .../exception/ManagementExceptionTests.java | 12 ++ .../json/jackson/JacksonJsonSerializer.java | 189 +---------------- .../jackson/JacksonJsonSerializerBuilder.java | 13 +- .../JacksonMemberNameConverterTests.java | 3 +- sdk/core/azure-core/pom.xml | 1 + .../jackson/MemberNameConverterImpl.java | 199 ++++++++++++++++++ .../jackson/ObjectMapperFactory.java | 4 +- .../jackson/ObjectMapperShim.java | 38 ++-- .../core/util/serializer/JacksonAdapter.java | 79 +++++-- .../azure-core/src/main/java/module-info.java | 3 + .../jackson/DateTimeDeserializerTests.java | 6 +- .../jackson/MemberNameConverterImplTests.java | 166 +++++++++++++++ .../jackson/ObjectMapperShimTests.java | 57 +++++ .../jackson/OptionSerializerTests.java | 9 +- .../azure-digitaltwins-core/pom.xml | 1 + .../azure-messaging-eventgrid/pom.xml | 1 + sdk/monitor/azure-monitor-query/pom.xml | 5 + .../azure-analytics-purview-catalog/pom.xml | 3 + .../azure-analytics-purview-scanning/pom.xml | 3 + sdk/search/azure-search-documents/pom.xml | 1 + .../azure-ai-documenttranslator/pom.xml | 3 + 26 files changed, 579 insertions(+), 237 deletions(-) create mode 100644 sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/MemberNameConverterImpl.java create mode 100644 sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/MemberNameConverterImplTests.java create mode 100644 sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/ObjectMapperShimTests.java diff --git a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml index ecf8041030041..060961b962ab7 100755 --- a/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml +++ b/eng/code-quality-reports/src/main/resources/checkstyle/checkstyle-suppressions.xml @@ -626,4 +626,8 @@ the main ServiceBusClientBuilder. --> + + + diff --git a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml index 3726620b1b49c..2f5b5c2e426b3 100755 --- a/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml +++ b/eng/code-quality-reports/src/main/resources/spotbugs/spotbugs-exclude.xml @@ -153,6 +153,13 @@ + + + + + + + diff --git a/sdk/agrifood/azure-verticals-agrifood-farming/pom.xml b/sdk/agrifood/azure-verticals-agrifood-farming/pom.xml index 63962ec632382..58c7633a4737b 100644 --- a/sdk/agrifood/azure-verticals-agrifood-farming/pom.xml +++ b/sdk/agrifood/azure-verticals-agrifood-farming/pom.xml @@ -32,6 +32,9 @@ true + + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED + diff --git a/sdk/confidentialledger/azure-security-confidentialledger/pom.xml b/sdk/confidentialledger/azure-security-confidentialledger/pom.xml index 5402b6dd84345..fe4186a0e02aa 100644 --- a/sdk/confidentialledger/azure-security-confidentialledger/pom.xml +++ b/sdk/confidentialledger/azure-security-confidentialledger/pom.xml @@ -32,6 +32,9 @@ true + + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED + diff --git a/sdk/core/azure-core-management/src/main/java/com/azure/core/management/implementation/serializer/AzureJacksonAdapter.java b/sdk/core/azure-core-management/src/main/java/com/azure/core/management/implementation/serializer/AzureJacksonAdapter.java index a2a6203ec72e9..306f77c55cf48 100644 --- a/sdk/core/azure-core-management/src/main/java/com/azure/core/management/implementation/serializer/AzureJacksonAdapter.java +++ b/sdk/core/azure-core-management/src/main/java/com/azure/core/management/implementation/serializer/AzureJacksonAdapter.java @@ -14,7 +14,6 @@ public final class AzureJacksonAdapter extends JacksonAdapter { * Creates an instance of the Azure flavored Jackson adapter. */ public AzureJacksonAdapter() { - super(); - serializer().registerModule(ManagementErrorDeserializer.getModule(simpleMapper())); + super((outerMapper, innerMapper) -> outerMapper.registerModule(ManagementErrorDeserializer.getModule(innerMapper))); } } diff --git a/sdk/core/azure-core-management/src/test/java/com/azure/core/management/exception/ManagementExceptionTests.java b/sdk/core/azure-core-management/src/test/java/com/azure/core/management/exception/ManagementExceptionTests.java index 10361b2f9d7e1..ed76206353601 100644 --- a/sdk/core/azure-core-management/src/test/java/com/azure/core/management/exception/ManagementExceptionTests.java +++ b/sdk/core/azure-core-management/src/test/java/com/azure/core/management/exception/ManagementExceptionTests.java @@ -43,6 +43,18 @@ public void testSubclassDeserialization() throws IOException { Assertions.assertEquals("ResourceGroupNotFound", managementError.getCode()); } + @Test + public void testCaseInsensitiveSubclassDeserialization() throws IOException { + final String errorBody = "{\"error\":{\"Code\":\"WepAppError\",\"MESSAGE\":\"Web app error.\",\"Details\":[{\"code\":\"e\"}],\"TaRgeT\":\"foo\"}}"; + + SerializerAdapter serializerAdapter = SerializerFactory.createDefaultManagementSerializerAdapter(); + WebError webError = serializerAdapter.deserialize(errorBody, WebError.class, SerializerEncoding.JSON); + Assertions.assertEquals("WepAppError", webError.getCode()); + Assertions.assertEquals("Web app error.", webError.getMessage()); + Assertions.assertEquals(1, webError.getDetails().size()); + Assertions.assertEquals("foo", webError.getTarget()); + } + @Test public void testDeserializationInResource() throws IOException { final String virtualMachineJson = "{\"properties\":{\"instanceView\":{\"patchStatus\":{\"availablePatchSummary\":{\"error\":{}}}}}}"; diff --git a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializer.java b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializer.java index eafd084a72cc2..6cf381c0524cc 100644 --- a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializer.java +++ b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializer.java @@ -3,96 +3,35 @@ package com.azure.core.serializer.json.jackson; -import com.azure.core.util.CoreUtils; +import com.azure.core.implementation.jackson.ObjectMapperShim; import com.azure.core.util.logging.ClientLogger; import com.azure.core.util.serializer.JsonSerializer; import com.azure.core.util.serializer.MemberNameConverter; import com.azure.core.util.serializer.TypeReference; -import com.fasterxml.jackson.annotation.JsonGetter; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.cfg.MapperConfig; -import com.fasterxml.jackson.databind.introspect.AnnotatedClass; -import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver; -import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; -import com.fasterxml.jackson.databind.introspect.VisibilityChecker; -import com.fasterxml.jackson.databind.type.TypeFactory; -import com.fasterxml.jackson.databind.util.BeanUtil; import reactor.core.publisher.Mono; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UncheckedIOException; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.lang.reflect.Field; import java.lang.reflect.Member; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; /** * Jackson based implementation of the {@link JsonSerializer} and {@link MemberNameConverter} interfaces. */ public final class JacksonJsonSerializer implements JsonSerializer, MemberNameConverter { - private static final String ACCESSOR_NAMING_STRATEGY = - "com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy"; - private static final String ACCESSOR_NAMING_STRATEGY_PROVIDER = ACCESSOR_NAMING_STRATEGY + ".Provider"; - private static final MethodHandle GET_ACCESSOR_NAMING; - private static final MethodHandle FOR_POJO; - private static final MethodHandle FIND_NAME_FOR_IS_GETTER; - private static final MethodHandle FIND_NAME_FOR_REGULAR_GETTER; - private static final boolean USE_REFLECTION_FOR_MEMBER_NAME; - - static { - MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); - - MethodHandle getAccessorNaming = null; - MethodHandle forPojo = null; - MethodHandle findNameForIsGetter = null; - MethodHandle findNameForRegularGetter = null; - boolean useReflectionForMemberName = false; - - try { - Class accessorNamingStrategyProviderClass = Class.forName(ACCESSOR_NAMING_STRATEGY_PROVIDER); - Class accessorNamingStrategyClass = Class.forName(ACCESSOR_NAMING_STRATEGY); - getAccessorNaming = publicLookup.findVirtual(MapperConfig.class, "getAccessorNaming", - MethodType.methodType(accessorNamingStrategyProviderClass)); - forPojo = publicLookup.findVirtual(accessorNamingStrategyProviderClass, "forPOJO", - MethodType.methodType(accessorNamingStrategyClass, MapperConfig.class, AnnotatedClass.class)); - findNameForIsGetter = publicLookup.findVirtual(accessorNamingStrategyClass, "findNameForIsGetter", - MethodType.methodType(String.class, AnnotatedMethod.class, String.class)); - findNameForRegularGetter = publicLookup.findVirtual(accessorNamingStrategyClass, "findNameForRegularGetter", - MethodType.methodType(String.class, AnnotatedMethod.class, String.class)); - useReflectionForMemberName = true; - } catch (Throwable ex) { - new ClientLogger(JacksonJsonSerializer.class) - .verbose("Failed to retrieve MethodHandles used to get naming strategy. Falling back to BeanUtils.", - ex); - } - - GET_ACCESSOR_NAMING = getAccessorNaming; - FOR_POJO = forPojo; - FIND_NAME_FOR_IS_GETTER = findNameForIsGetter; - FIND_NAME_FOR_REGULAR_GETTER = findNameForRegularGetter; - USE_REFLECTION_FOR_MEMBER_NAME = useReflectionForMemberName; - } private final ClientLogger logger = new ClientLogger(JacksonJsonSerializer.class); - private final ObjectMapper mapper; - private final TypeFactory typeFactory; + private final ObjectMapperShim mapper; /** * Constructs a {@link JsonSerializer} using the passed Jackson serializer. * * @param mapper Configured Jackson serializer. */ - JacksonJsonSerializer(ObjectMapper mapper) { + JacksonJsonSerializer(ObjectMapperShim mapper) { this.mapper = mapper; - this.typeFactory = mapper.getTypeFactory(); } @Override @@ -102,7 +41,7 @@ public T deserializeFromBytes(byte[] data, TypeReference typeReference) { } try { - return mapper.readValue(data, typeFactory.constructType(typeReference.getJavaType())); + return mapper.readValue(data, typeReference.getJavaType()); } catch (IOException ex) { throw logger.logExceptionAsError(new UncheckedIOException(ex)); } @@ -115,7 +54,7 @@ public T deserialize(InputStream stream, TypeReference typeReference) { } try { - return mapper.readValue(stream, typeFactory.constructType(typeReference.getJavaType())); + return mapper.readValue(stream, typeReference.getJavaType()); } catch (IOException ex) { throw logger.logExceptionAsError(new UncheckedIOException(ex)); } @@ -159,124 +98,8 @@ public Mono serializeAsync(OutputStream stream, Object value) { return Mono.fromRunnable(() -> serialize(stream, value)); } - @Override public String convertMemberName(Member member) { - if (Modifier.isTransient(member.getModifiers())) { - return null; - } - - VisibilityChecker visibilityChecker = mapper.getVisibilityChecker(); - if (member instanceof Field) { - Field f = (Field) member; - - if (f.isAnnotationPresent(JsonIgnore.class) || !visibilityChecker.isFieldVisible(f)) { - if (f.isAnnotationPresent(JsonProperty.class)) { - logger.info("Field {} is annotated with JsonProperty but isn't accessible to " - + "JacksonJsonSerializer.", f.getName()); - } - return null; - } - - if (f.isAnnotationPresent(JsonProperty.class)) { - String propertyName = f.getDeclaredAnnotation(JsonProperty.class).value(); - return CoreUtils.isNullOrEmpty(propertyName) ? f.getName() : propertyName; - } - - return f.getName(); - } - - if (member instanceof Method) { - Method m = (Method) member; - - /* - * If the method isn't a getter, is annotated with JsonIgnore, or isn't visible to the ObjectMapper ignore - * it. - */ - if (!verifyGetter(m) - || m.isAnnotationPresent(JsonIgnore.class) - || !visibilityChecker.isGetterVisible(m)) { - if (m.isAnnotationPresent(JsonGetter.class) || m.isAnnotationPresent(JsonProperty.class)) { - logger.info("Method {} is annotated with either JsonGetter or JsonProperty but isn't accessible " - + "to JacksonJsonSerializer.", m.getName()); - } - return null; - } - - String methodNameWithoutJavaBeans = removePrefix(m); - - /* - * Prefer JsonGetter over JsonProperty as it is the more targeted annotation. - */ - if (m.isAnnotationPresent(JsonGetter.class)) { - String propertyName = m.getDeclaredAnnotation(JsonGetter.class).value(); - return CoreUtils.isNullOrEmpty(propertyName) ? methodNameWithoutJavaBeans : propertyName; - } - - if (m.isAnnotationPresent(JsonProperty.class)) { - String propertyName = m.getDeclaredAnnotation(JsonProperty.class).value(); - return CoreUtils.isNullOrEmpty(propertyName) ? methodNameWithoutJavaBeans : propertyName; - } - - // If no annotation is present default to the inferred name. - return methodNameWithoutJavaBeans; - } - - return null; - } - - /* - * Only consider methods that don't have parameters and aren't void as valid getter methods. - */ - private static boolean verifyGetter(Method method) { - Class returnType = method.getReturnType(); - - return method.getParameterCount() == 0 - && returnType != void.class - && returnType != Void.class; - } - - private String removePrefix(Method method) { - MapperConfig config = mapper.getSerializationConfig(); - - AnnotatedClass annotatedClass = AnnotatedClassResolver.resolve(config, - mapper.constructType(method.getDeclaringClass()), null); - - AnnotatedMethod annotatedMethod = new AnnotatedMethod(null, method, null, null); - String annotatedMethodName = annotatedMethod.getName(); - - String name = null; - if (USE_REFLECTION_FOR_MEMBER_NAME) { - name = removePrefixWithReflection(config, annotatedClass, annotatedMethod, annotatedMethodName, logger); - } - - if (name == null) { - name = removePrefixWithBeanUtils(annotatedMethod); - } - - return name; - } - - private static String removePrefixWithReflection(MapperConfig config, AnnotatedClass annotatedClass, - AnnotatedMethod method, String methodName, ClientLogger logger) { - try { - Object accessorNamingStrategy = FOR_POJO.invoke(GET_ACCESSOR_NAMING.invoke(config), config, annotatedClass); - - - String name = (String) FIND_NAME_FOR_IS_GETTER.invoke(accessorNamingStrategy, method, methodName); - if (name == null) { - name = (String) FIND_NAME_FOR_REGULAR_GETTER.invoke(accessorNamingStrategy, method, methodName); - } - - return name; - } catch (Throwable ex) { - logger.verbose("Failed to find member name with AccessorNamingStrategy, returning null.", ex); - return null; - } - } - - @SuppressWarnings("deprecation") - private static String removePrefixWithBeanUtils(AnnotatedMethod annotatedMethod) { - return BeanUtil.okNameForGetter(annotatedMethod, false); + return mapper.convertMemberName(member); } } diff --git a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializerBuilder.java b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializerBuilder.java index 12c7889b138ef..5256133bae3e4 100644 --- a/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializerBuilder.java +++ b/sdk/core/azure-core-serializer-json-jackson/src/main/java/com/azure/core/serializer/json/jackson/JacksonJsonSerializerBuilder.java @@ -3,6 +3,7 @@ package com.azure.core.serializer.json.jackson; +import com.azure.core.implementation.jackson.ObjectMapperShim; import com.azure.core.util.serializer.JacksonAdapter; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonInclude; @@ -17,11 +18,13 @@ public final class JacksonJsonSerializerBuilder { * Jackson uses. This configuration is reset here by mutating the inclusion scope and null handling to use the * default Jackson values so that JacksonJsonSerializer has less friction when this default is used. */ - private static final ObjectMapper DEFAULT_MAPPER = new JacksonAdapter().serializer() - .setSerializationInclusion(JsonInclude.Include.USE_DEFAULTS) - .setDefaultVisibility(JsonAutoDetect.Value.defaultVisibility()); + private static final ObjectMapperShim DEFAULT_MAPPER = ObjectMapperShim + .createJsonMapper(ObjectMapperShim.createSimpleMapper(), + (mapper, innerMapper) -> mapper + .setSerializationInclusion(JsonInclude.Include.USE_DEFAULTS) + .setDefaultVisibility(JsonAutoDetect.Value.defaultVisibility())); - private ObjectMapper objectMapper; + private ObjectMapperShim objectMapper; /** * Constructs a new instance of {@link JacksonJsonSerializer} with the configurations set in this builder. @@ -44,7 +47,7 @@ public JacksonJsonSerializer build() { * @return The updated JacksonJsonSerializerBuilder class. */ public JacksonJsonSerializerBuilder serializer(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + this.objectMapper = new ObjectMapperShim(objectMapper); return this; } } diff --git a/sdk/core/azure-core-serializer-json-jackson/src/test/java/com/azure/core/serializer/json/jackson/JacksonMemberNameConverterTests.java b/sdk/core/azure-core-serializer-json-jackson/src/test/java/com/azure/core/serializer/json/jackson/JacksonMemberNameConverterTests.java index ec9ca46b88a13..4a8d65c720e76 100644 --- a/sdk/core/azure-core-serializer-json-jackson/src/test/java/com/azure/core/serializer/json/jackson/JacksonMemberNameConverterTests.java +++ b/sdk/core/azure-core-serializer-json-jackson/src/test/java/com/azure/core/serializer/json/jackson/JacksonMemberNameConverterTests.java @@ -3,6 +3,7 @@ package com.azure.core.serializer.json.jackson; +import com.azure.core.implementation.jackson.ObjectMapperShim; import com.azure.core.util.serializer.MemberNameConverter; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -284,7 +285,7 @@ public void classConversion(T object, JacksonJsonSerializer converter, Set --add-exports com.azure.core/com.azure.core.implementation.http=ALL-UNNAMED --add-exports com.azure.core/com.azure.core.implementation.serializer=ALL-UNNAMED + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED --add-opens com.azure.core/com.azure.core=ALL-UNNAMED --add-opens com.azure.core/com.azure.core.credential=ALL-UNNAMED diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/MemberNameConverterImpl.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/MemberNameConverterImpl.java new file mode 100644 index 0000000000000..486cf24decd85 --- /dev/null +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/MemberNameConverterImpl.java @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.implementation.jackson; + +import com.azure.core.util.CoreUtils; +import com.azure.core.util.logging.ClientLogger; +import com.azure.core.util.serializer.MemberNameConverter; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; +import com.fasterxml.jackson.databind.introspect.AnnotatedClassResolver; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.VisibilityChecker; +import com.fasterxml.jackson.databind.util.BeanUtil; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * Retrieves the JSON serialized property name from {@link Member}. + */ +final class MemberNameConverterImpl implements MemberNameConverter { + private static final ClientLogger LOGGER = new ClientLogger(MemberNameConverterImpl.class); + + private static final String ACCESSOR_NAMING_STRATEGY = + "com.fasterxml.jackson.databind.introspect.AccessorNamingStrategy"; + private static final String ACCESSOR_NAMING_STRATEGY_PROVIDER = ACCESSOR_NAMING_STRATEGY + ".Provider"; + private static final MethodHandle GET_ACCESSOR_NAMING; + private static final MethodHandle FOR_POJO; + private static final MethodHandle FIND_NAME_FOR_IS_GETTER; + private static final MethodHandle FIND_NAME_FOR_REGULAR_GETTER; + private static final boolean USE_REFLECTION_FOR_MEMBER_NAME; + + private final ObjectMapper mapper; + + static { + MethodHandles.Lookup publicLookup = MethodHandles.publicLookup(); + + MethodHandle getAccessorNaming = null; + MethodHandle forPojo = null; + MethodHandle findNameForIsGetter = null; + MethodHandle findNameForRegularGetter = null; + boolean useReflectionForMemberName = false; + + try { + Class accessorNamingStrategyProviderClass = Class.forName(ACCESSOR_NAMING_STRATEGY_PROVIDER); + Class accessorNamingStrategyClass = Class.forName(ACCESSOR_NAMING_STRATEGY); + getAccessorNaming = publicLookup.findVirtual(MapperConfig.class, "getAccessorNaming", + MethodType.methodType(accessorNamingStrategyProviderClass)); + forPojo = publicLookup.findVirtual(accessorNamingStrategyProviderClass, "forPOJO", + MethodType.methodType(accessorNamingStrategyClass, MapperConfig.class, AnnotatedClass.class)); + findNameForIsGetter = publicLookup.findVirtual(accessorNamingStrategyClass, "findNameForIsGetter", + MethodType.methodType(String.class, AnnotatedMethod.class, String.class)); + findNameForRegularGetter = publicLookup.findVirtual(accessorNamingStrategyClass, "findNameForRegularGetter", + MethodType.methodType(String.class, AnnotatedMethod.class, String.class)); + useReflectionForMemberName = true; + } catch (Throwable ex) { + LOGGER.verbose("Failed to retrieve MethodHandles used to get naming strategy. Falling back to BeanUtils.", + ex); + } + + GET_ACCESSOR_NAMING = getAccessorNaming; + FOR_POJO = forPojo; + FIND_NAME_FOR_IS_GETTER = findNameForIsGetter; + FIND_NAME_FOR_REGULAR_GETTER = findNameForRegularGetter; + USE_REFLECTION_FOR_MEMBER_NAME = useReflectionForMemberName; + } + + MemberNameConverterImpl(ObjectMapper mapper) { + this.mapper = mapper; + } + + @Override + public String convertMemberName(Member member) { + if (Modifier.isTransient(member.getModifiers())) { + return null; + } + + VisibilityChecker visibilityChecker = mapper.getVisibilityChecker(); + if (member instanceof Field) { + Field f = (Field) member; + + if (f.isAnnotationPresent(JsonIgnore.class) || !visibilityChecker.isFieldVisible(f)) { + if (f.isAnnotationPresent(JsonProperty.class)) { + LOGGER.info("Field {} is annotated with JsonProperty but isn't accessible to " + + "JacksonJsonSerializer.", f.getName()); + } + return null; + } + + if (f.isAnnotationPresent(JsonProperty.class)) { + String propertyName = f.getDeclaredAnnotation(JsonProperty.class).value(); + return CoreUtils.isNullOrEmpty(propertyName) ? f.getName() : propertyName; + } + + return f.getName(); + } + + if (member instanceof Method) { + Method m = (Method) member; + + /* + * If the method isn't a getter, is annotated with JsonIgnore, or isn't visible to the ObjectMapper ignore + * it. + */ + if (!verifyGetter(m) + || m.isAnnotationPresent(JsonIgnore.class) + || !visibilityChecker.isGetterVisible(m)) { + if (m.isAnnotationPresent(JsonGetter.class) || m.isAnnotationPresent(JsonProperty.class)) { + LOGGER.info("Method {} is annotated with either JsonGetter or JsonProperty but isn't accessible " + + "to JacksonJsonSerializer.", m.getName()); + } + return null; + } + + String methodNameWithoutJavaBeans = removePrefix(m); + + /* + * Prefer JsonGetter over JsonProperty as it is the more targeted annotation. + */ + if (m.isAnnotationPresent(JsonGetter.class)) { + String propertyName = m.getDeclaredAnnotation(JsonGetter.class).value(); + return CoreUtils.isNullOrEmpty(propertyName) ? methodNameWithoutJavaBeans : propertyName; + } + + if (m.isAnnotationPresent(JsonProperty.class)) { + String propertyName = m.getDeclaredAnnotation(JsonProperty.class).value(); + return CoreUtils.isNullOrEmpty(propertyName) ? methodNameWithoutJavaBeans : propertyName; + } + + // If no annotation is present default to the inferred name. + return methodNameWithoutJavaBeans; + } + + return null; + } + + /* + * Only consider methods that don't have parameters and aren't void as valid getter methods. + */ + private static boolean verifyGetter(Method method) { + Class returnType = method.getReturnType(); + + return method.getParameterCount() == 0 + && returnType != void.class + && returnType != Void.class; + } + + private String removePrefix(Method method) { + MapperConfig config = mapper.getSerializationConfig(); + + AnnotatedClass annotatedClass = AnnotatedClassResolver.resolve(config, + mapper.constructType(method.getDeclaringClass()), null); + + AnnotatedMethod annotatedMethod = new AnnotatedMethod(null, method, null, null); + String annotatedMethodName = annotatedMethod.getName(); + + String name = null; + if (USE_REFLECTION_FOR_MEMBER_NAME) { + name = removePrefixWithReflection(config, annotatedClass, annotatedMethod, annotatedMethodName); + } + + if (name == null) { + name = removePrefixWithBeanUtils(annotatedMethod); + } + + return name; + } + + private static String removePrefixWithReflection(MapperConfig config, AnnotatedClass annotatedClass, + AnnotatedMethod method, String methodName) { + try { + Object accessorNamingStrategy = FOR_POJO.invoke(GET_ACCESSOR_NAMING.invoke(config), config, annotatedClass); + String name = (String) FIND_NAME_FOR_IS_GETTER.invoke(accessorNamingStrategy, method, methodName); + if (name == null) { + name = (String) FIND_NAME_FOR_REGULAR_GETTER.invoke(accessorNamingStrategy, method, methodName); + } + + return name; + } catch (Throwable ex) { + LOGGER.verbose("Failed to find member name with AccessorNamingStrategy, returning null.", ex); + return null; + } + } + + @SuppressWarnings("deprecation") + private static String removePrefixWithBeanUtils(AnnotatedMethod annotatedMethod) { + return BeanUtil.okNameForGetter(annotatedMethod, false); + } +} diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/ObjectMapperFactory.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/ObjectMapperFactory.java index 68d69ceb2a102..2ce8a2d8b54ad 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/ObjectMapperFactory.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/ObjectMapperFactory.java @@ -65,8 +65,7 @@ private ObjectMapperFactory() { } } - public ObjectMapper createJsonMapper(ObjectMapperShim innerMapperShim) { - ObjectMapper innerMapper = innerMapperShim.getMapper(); + public ObjectMapper createJsonMapper(ObjectMapper innerMapper) { ObjectMapper flatteningMapper = initializeMapperBuilder(JsonMapper.builder()) .addModule(FlatteningSerializer.getModule(innerMapper)) .addModule(FlatteningDeserializer.getModule(innerMapper)) @@ -92,7 +91,6 @@ public ObjectMapper createXmlMapper() { .enable(FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL) .build(); - if (useReflectionToSetCoercion) { try { Object object = coersionConfigDefaults.invoke(xmlMapper); diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/ObjectMapperShim.java b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/ObjectMapperShim.java index a4da8e40e5bca..8b91e089253b8 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/ObjectMapperShim.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/implementation/jackson/ObjectMapperShim.java @@ -16,6 +16,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.Field; +import java.lang.reflect.Member; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; @@ -25,6 +26,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiConsumer; import java.util.function.Function; /** @@ -45,11 +47,13 @@ public final class ObjectMapperShim { * Creates and configures JSON {@code ObjectMapper} capable of serializing azure.core types, with flattening and additional properties support. * * @param innerMapperShim inner mapper to use for non-azure specific serialization. + * @param configure applies additional configuration to {@code ObjectMapper}. * @return Instance of shimmed {@code ObjectMapperShim}. */ - public static ObjectMapperShim createJsonMapper(ObjectMapperShim innerMapperShim) { + public static ObjectMapperShim createJsonMapper(ObjectMapperShim innerMapperShim, BiConsumer configure) { try { - ObjectMapper mapper = ObjectMapperFactory.INSTANCE.createJsonMapper(innerMapperShim); + ObjectMapper mapper = ObjectMapperFactory.INSTANCE.createJsonMapper(innerMapperShim.mapper); + configure.accept(mapper, innerMapperShim.mapper); return new ObjectMapperShim(mapper); } catch (LinkageError ex) { throw LOGGER.logThrowableAsError(new LinkageError(JACKSON_VERSION.getHelpInfo(), ex)); @@ -127,17 +131,12 @@ public static ObjectMapperShim createHeaderMapper() { } private final ObjectMapper mapper; + private final MemberNameConverterImpl memberNameConverter; - private ObjectMapperShim(ObjectMapper mapper) { - this.mapper = mapper; - } - /** - * Gets wrapped {@code ObjectMapper} instance. Use with caution. - * @return - */ - public ObjectMapper getMapper() { - return this.mapper; + public ObjectMapperShim(ObjectMapper mapper) { + this.mapper = mapper; + this.memberNameConverter = new MemberNameConverterImpl(mapper); } /** @@ -254,7 +253,6 @@ public JsonNode readTree(String content) throws IOException { * Reads JSON tree from byte array. * @param content serialized JSON tree. * @return {@code JsonNode} instance - * @throws IOException */ public JsonNode readTree(byte[] content) throws IOException { try { @@ -366,6 +364,22 @@ public T deserialize(HttpHeaders headers, Type deserializedHeadersType) thro return deserializedHeaders; } + public String convertMemberName(Member member) { + try { + return memberNameConverter.convertMemberName(member); + } catch (LinkageError ex) { + throw LOGGER.logThrowableAsError(new LinkageError(JACKSON_VERSION.getHelpInfo(), ex)); + } + } + + public T valueToTree(Object fromValue) { + try { + return mapper.valueToTree(fromValue); + } catch (LinkageError ex) { + throw LOGGER.logThrowableAsError(new LinkageError(JACKSON_VERSION.getHelpInfo(), ex)); + } + } + /* * Helper method that gets the value for the given key from the cache. */ diff --git a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java index c3cafb27759bd..71103b83faa04 100644 --- a/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java +++ b/sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/JacksonAdapter.java @@ -14,6 +14,8 @@ import java.io.OutputStream; import java.lang.reflect.Type; import java.util.List; +import java.util.Objects; +import java.util.function.BiConsumer; import java.util.regex.Pattern; /** @@ -28,15 +30,17 @@ public class JacksonAdapter implements SerializerAdapter { */ private final ObjectMapperShim mapper; - /** - * An instance of {@link ObjectMapperShim} that does not do flattening. - */ - private final ObjectMapperShim simpleMapper; - private final ObjectMapperShim xmlMapper; private final ObjectMapperShim headerMapper; + /** + * Raw mappers are needed only to support deprecated simpleMapper() and + * serializer(). + */ + private ObjectMapper rawOuterMapper; + private ObjectMapper rawInnerMapper; + /* * The lazily-created serializer for this ServiceClient. */ @@ -46,19 +50,55 @@ public class JacksonAdapter implements SerializerAdapter { * Creates a new JacksonAdapter instance with default mapper settings. */ public JacksonAdapter() { - this.simpleMapper = ObjectMapperShim.createSimpleMapper(); + this((outerMapper, innerMapper) -> { }); + } + + /** + * Creates a new JacksonAdapter instance with Azure Core mapper settings and applies + * additional configuration through {@code configureSerialization} callback. + * + * {@code configureSerialization} callback provides outer and inner instances of {@link ObjectMapper}. + * Both of them are pre-configured for Azure serialization needs, but only outer mapper capable of + * flattening and populating additionalProperties. Outer mapper is used by {@code JacksonAdapter} for + * all serialization needs. + * + * Register modules on the outer instance to add custom (de)serializers similar to + * {@code new JacksonAdapter((outer, inner) -> outer.registerModule(new MyModule()))} + * + * Use inner mapper for chaining serialization logic in your (de)serializers. + * + * @param configureSerialization Applies additional configuration to outer + * mapper using inner mapper for module chaining. + */ + public JacksonAdapter(BiConsumer configureSerialization) { + Objects.requireNonNull(configureSerialization, "'configureSerialization' cannot be null."); this.headerMapper = ObjectMapperShim.createHeaderMapper(); this.xmlMapper = ObjectMapperShim.createXmlMapper(); - this.mapper = ObjectMapperShim.createJsonMapper(this.simpleMapper); + this.mapper = ObjectMapperShim.createJsonMapper(ObjectMapperShim.createSimpleMapper(), + (outerMapper, innerMapper) -> captureRawMappersAndConfigure(outerMapper, innerMapper, configureSerialization)); + } + + /** + * Temporary way to capture raw ObjectMapper instances, allows to support deprecated simpleMapper() + * and serializer() + */ + private void captureRawMappersAndConfigure(ObjectMapper outerMapper, ObjectMapper innerMapper, BiConsumer configure) { + this.rawOuterMapper = outerMapper; + this.rawInnerMapper = innerMapper; + + configure.accept(outerMapper, innerMapper); } /** * Gets a static instance of {@link ObjectMapper} that doesn't handle flattening. * * @return an instance of {@link ObjectMapper}. + * @deprecated deprecated, use {@code JacksonAdapter(BiConsumer)} constructor to + * configure modules. */ + @Deprecated protected ObjectMapper simpleMapper() { - return simpleMapper.getMapper(); + return rawInnerMapper; } /** @@ -74,14 +114,13 @@ public static synchronized SerializerAdapter createDefaultSerializerAdapter() { } /** - * @return the original serializer type + * @return the original serializer type. + * @deprecated deprecated to avoid direct {@link ObjectMapper} usage in favor + * of using more resilient and debuggable {@link JacksonAdapter} APIs. */ + @Deprecated public ObjectMapper serializer() { - return mapper.getMapper(); - } - - private ObjectMapperShim serializerShim() { - return mapper; + return rawOuterMapper; } @Override @@ -93,7 +132,7 @@ public String serialize(Object object, SerializerEncoding encoding) throws IOExc if (encoding == SerializerEncoding.XML) { return xmlMapper.writeValueAsString(object); } else { - return serializerShim().writeValueAsString(object); + return mapper.writeValueAsString(object); } } @@ -106,7 +145,7 @@ public byte[] serializeToBytes(Object object, SerializerEncoding encoding) throw if (encoding == SerializerEncoding.XML) { return xmlMapper.writeValueAsBytes(object); } else { - return serializerShim() .writeValueAsBytes(object); + return mapper.writeValueAsBytes(object); } } @@ -119,7 +158,7 @@ public void serialize(Object object, SerializerEncoding encoding, OutputStream o if ((encoding == SerializerEncoding.XML)) { xmlMapper.writeValue(outputStream, object); } else { - serializerShim().writeValue(outputStream, object); + mapper.writeValue(outputStream, object); } } @@ -151,7 +190,7 @@ public T deserialize(String value, Type type, SerializerEncoding encoding) t if (encoding == SerializerEncoding.XML) { return xmlMapper.readValue(value, type); } else { - return serializerShim().readValue(value, type); + return mapper.readValue(value, type); } } @@ -164,7 +203,7 @@ public T deserialize(byte[] bytes, Type type, SerializerEncoding encoding) t if (encoding == SerializerEncoding.XML) { return xmlMapper.readValue(bytes, type); } else { - return serializerShim().readValue(bytes, type); + return mapper.readValue(bytes, type); } } @@ -178,7 +217,7 @@ public T deserialize(InputStream inputStream, final Type type, SerializerEnc if (encoding == SerializerEncoding.XML) { return xmlMapper.readValue(inputStream, type); } else { - return serializerShim().readValue(inputStream, type); + return mapper.readValue(inputStream, type); } } diff --git a/sdk/core/azure-core/src/main/java/module-info.java b/sdk/core/azure-core/src/main/java/module-info.java index 1c07b3dae87df..a78cf200ce96e 100644 --- a/sdk/core/azure-core/src/main/java/module-info.java +++ b/sdk/core/azure-core/src/main/java/module-info.java @@ -29,6 +29,9 @@ exports com.azure.core.util.serializer; exports com.azure.core.util.tracing; + // TODO temporary until we find final shape of ObjectMapper shimming APIs + exports com.azure.core.implementation.jackson to com.azure.core.management, com.azure.core.serializer.json.jackson; + // exporting some packages specifically for Jackson opens com.azure.core.http to com.fasterxml.jackson.databind; opens com.azure.core.models to com.fasterxml.jackson.databind; diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/DateTimeDeserializerTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/DateTimeDeserializerTests.java index 0634e84083720..3c8e70c4543ec 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/DateTimeDeserializerTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/DateTimeDeserializerTests.java @@ -4,7 +4,7 @@ package com.azure.core.implementation.jackson; import com.azure.core.util.serializer.JacksonAdapter; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.azure.core.util.serializer.SerializerEncoding; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -20,12 +20,12 @@ * Tests for {@link DateTimeDeserializer}. */ public class DateTimeDeserializerTests { - private static final ObjectMapper MAPPER = new JacksonAdapter().serializer(); + private static final JacksonAdapter MAPPER = new JacksonAdapter(); @ParameterizedTest @MethodSource("deserializeOffsetDateTimeSupplier") public void deserializeJson(String dateTimeJson, OffsetDateTime expected) throws IOException { - assertEquals(expected, MAPPER.readValue(dateTimeJson, OffsetDateTime.class)); + assertEquals(expected, MAPPER.deserialize(dateTimeJson, OffsetDateTime.class, SerializerEncoding.JSON)); } private static Stream deserializeOffsetDateTimeSupplier() { diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/MemberNameConverterImplTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/MemberNameConverterImplTests.java new file mode 100644 index 0000000000000..ba4a97a600165 --- /dev/null +++ b/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/MemberNameConverterImplTests.java @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.implementation.jackson; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class MemberNameConverterImplTests { + @Test + public void fieldsWithJsonProperty() throws NoSuchFieldException { + final Field publicFieldWithAnnotationAndValue = Foo.class.getDeclaredField("publicFieldWithAnnotationAndValue"); + final Field publicFieldWithAnnotationNoValue = Foo.class.getDeclaredField("publicFieldWithAnnotationNoValue"); + final Field privateFieldWithAnnotation = Foo.class.getDeclaredField("privateFieldWithAnnotation"); + + MemberNameConverterImpl memberNameConverter = new MemberNameConverterImpl(new ObjectMapper()); + + assertEquals("public-field-with-annotation-and-value", memberNameConverter.convertMemberName(publicFieldWithAnnotationAndValue)); + assertEquals("publicFieldWithAnnotationNoValue", memberNameConverter.convertMemberName(publicFieldWithAnnotationNoValue)); + assertNull(memberNameConverter.convertMemberName(privateFieldWithAnnotation)); + } + + @Test + public void methodsWithJsonGetter() throws NoSuchMethodException { + final Method getPublicWithAnnotationAndValue = Foo.class.getDeclaredMethod("getPublicWithAnnotationAndValue"); + final Method getPublicWithAnnotationNoValue = Foo.class.getDeclaredMethod("getPublicWithAnnotationNoValue"); + final Method publicWithAnnotationNoPrefix = Foo.class.getDeclaredMethod("publicWithAnnotationNoPrefix"); + final Method isPublicWithAnnotationString = Foo.class.getDeclaredMethod("isPublicWithAnnotationString"); + final Method isPublicWithAnnotationBoolean = Foo.class.getDeclaredMethod("isPublicWithAnnotationBoolean"); + final Method getPrivateWithAnnotation = Foo.class.getDeclaredMethod("getPrivateWithAnnotation"); + final Method getVoidWithAnnotation = Foo.class.getDeclaredMethod("getVoidWithAnnotation"); + + MemberNameConverterImpl memberNameConverter = new MemberNameConverterImpl(new ObjectMapper()); + + assertEquals("public-getter-with-annotation-and-value", memberNameConverter.convertMemberName(getPublicWithAnnotationAndValue)); + assertEquals("publicWithAnnotationNoValue", memberNameConverter.convertMemberName(getPublicWithAnnotationNoValue)); + assertEquals("publicWithAnnotationBoolean", memberNameConverter.convertMemberName(isPublicWithAnnotationBoolean)); + assertNull(memberNameConverter.convertMemberName(publicWithAnnotationNoPrefix)); + assertNull(memberNameConverter.convertMemberName(isPublicWithAnnotationString)); + assertNull(memberNameConverter.convertMemberName(getPrivateWithAnnotation)); + assertNull(memberNameConverter.convertMemberName(getVoidWithAnnotation)); + } + + @Test + public void fieldNoJsonProperty() throws NoSuchFieldException { + final Field publicIgnoredField = Foo.class.getDeclaredField("publicIgnoredField"); + final Field publicNoAnnotationField = Foo.class.getDeclaredField("publicNoAnnotationField"); + final Field privateField = Foo.class.getDeclaredField("privateField"); + + MemberNameConverterImpl memberNameConverter = new MemberNameConverterImpl(new ObjectMapper()); + + assertEquals("publicNoAnnotationField", memberNameConverter.convertMemberName(publicNoAnnotationField)); + assertNull(memberNameConverter.convertMemberName(publicIgnoredField)); + assertNull(memberNameConverter.convertMemberName(privateField)); + } + + @Test + public void methodsNoJsonGetter() throws NoSuchMethodException { + final Method getPublicIgnored = Foo.class.getDeclaredMethod("getPublicIgnored"); + final Method getPublicNoAnnotation = Foo.class.getDeclaredMethod("getPublicNoAnnotation"); + final Method publicNoAnnotationNoPrefix = Foo.class.getDeclaredMethod("publicNoAnnotationNoPrefix"); + final Method isPublicNoAnnotationString = Foo.class.getDeclaredMethod("isPublicNoAnnotationString"); + final Method isPublicNoAnnotationBoolean = Foo.class.getDeclaredMethod("isPublicNoAnnotationBoolean"); + final Method getNoAnnotationVoid = Foo.class.getDeclaredMethod("getNoAnnotationVoid"); + final Method getNoAnnotationPrivate = Foo.class.getDeclaredMethod("getNoAnnotationPrivate"); + + MemberNameConverterImpl memberNameConverter = new MemberNameConverterImpl(new ObjectMapper()); + + assertEquals("publicNoAnnotation", memberNameConverter.convertMemberName(getPublicNoAnnotation)); + assertEquals("publicNoAnnotationBoolean", memberNameConverter.convertMemberName(isPublicNoAnnotationBoolean)); + assertNull(memberNameConverter.convertMemberName(getPublicIgnored)); + assertNull(memberNameConverter.convertMemberName(publicNoAnnotationNoPrefix)); + assertNull(memberNameConverter.convertMemberName(isPublicNoAnnotationString)); + assertNull(memberNameConverter.convertMemberName(getNoAnnotationVoid)); + assertNull(memberNameConverter.convertMemberName(getNoAnnotationPrivate)); + } + + private static class Foo { + @JsonProperty(value = "public-field-with-annotation-and-value") + public String publicFieldWithAnnotationAndValue; + + @JsonProperty + public String publicFieldWithAnnotationNoValue; + + @JsonIgnore + public String publicIgnoredField; + + public String publicNoAnnotationField; + + @JsonProperty + private String privateFieldWithAnnotation = null; + + private String privateField = null; + + @JsonGetter(value = "public-getter-with-annotation-and-value") + public String getPublicWithAnnotationAndValue() { + return "foo"; + } + + @JsonGetter + public String getPublicWithAnnotationNoValue() { + return "foo"; + } + + @JsonGetter + public void getVoidWithAnnotation() { + } + + @JsonGetter + public String publicWithAnnotationNoPrefix() { + return "foo"; + } + + @JsonGetter + public String isPublicWithAnnotationString() { + return "foo"; + } + + @JsonGetter + public boolean isPublicWithAnnotationBoolean() { + return true; + } + + @JsonIgnore + public String getPublicIgnored() { + return "foo"; + } + + public String getPublicNoAnnotation() { + return "foo"; + } + + public String publicNoAnnotationNoPrefix() { + return "foo"; + } + + public String isPublicNoAnnotationString() { + return "foo"; + } + + public boolean isPublicNoAnnotationBoolean() { + return true; + } + + public void getNoAnnotationVoid() { + } + + @JsonGetter + private String getPrivateWithAnnotation() { + return "foo"; + } + + private String getNoAnnotationPrivate() { + return "foo"; + } + } +} diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/ObjectMapperShimTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/ObjectMapperShimTests.java new file mode 100644 index 0000000000000..302de70b12c3f --- /dev/null +++ b/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/ObjectMapperShimTests.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.core.implementation.jackson; + +import com.azure.core.util.serializer.JacksonAdapter; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ObjectMapperShimTests { + @Test + public void testConfigure() { + final ObjectMapper innerMapper = new ObjectMapper(); + final ObjectMapperShim innerShim = new ObjectMapperShim(innerMapper); + + final AtomicReference configureIsCalled = new AtomicReference<>(false); + ObjectMapperShim.createJsonMapper(innerShim, (outer, inner) -> { + assertNotNull(outer); + assertSame(innerMapper, inner); + configureIsCalled.set(true); + }); + + assertTrue(configureIsCalled.get()); + } + + @Test + @SuppressWarnings("deprecation") + public void testConfigureJacksonAdapter() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + final AtomicReference configureIsCalled = new AtomicReference<>(false); + final AtomicReference outerMapper = new AtomicReference<>(null); + final AtomicReference innerMapper = new AtomicReference<>(null); + + final JacksonAdapter adapter = new JacksonAdapter((outer, inner) -> { + outerMapper.set(outer); + innerMapper.set(inner); + assertNotNull(outer); + assertNotNull(inner); + configureIsCalled.set(true); + }); + + assertTrue(configureIsCalled.get()); + assertSame(outerMapper.get(), adapter.serializer()); + + // check protected simpleMapper getter + Method method = JacksonAdapter.class.getDeclaredMethod("simpleMapper"); + method.setAccessible(true); + assertSame(innerMapper.get(), method.invoke(adapter)); + } +} diff --git a/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/OptionSerializerTests.java b/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/OptionSerializerTests.java index 45e534e00463c..f895b28fb0e2e 100644 --- a/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/OptionSerializerTests.java +++ b/sdk/core/azure-core/src/test/java/com/azure/core/implementation/jackson/OptionSerializerTests.java @@ -16,14 +16,7 @@ * Tests for {@link Option} that can represent tri-sate (non-null-value, null-value, or no-value). */ public class OptionSerializerTests { - private static final JacksonAdapter ADAPTER; - - static { - JacksonAdapter adapter = new JacksonAdapter(); - adapter.serializer().registerModule(new OptionModule()); - - ADAPTER = adapter; - } + private static final JacksonAdapter ADAPTER = new JacksonAdapter((outerMapper, innerMapper) -> outerMapper.registerModule(new OptionModule())); @Test public void canSerializeExplicitNull() throws IOException { diff --git a/sdk/digitaltwins/azure-digitaltwins-core/pom.xml b/sdk/digitaltwins/azure-digitaltwins-core/pom.xml index c3bdf25eece74..8e39d35eea2e2 100644 --- a/sdk/digitaltwins/azure-digitaltwins-core/pom.xml +++ b/sdk/digitaltwins/azure-digitaltwins-core/pom.xml @@ -146,6 +146,7 @@ --add-exports com.azure.core/com.azure.core.implementation.http=ALL-UNNAMED + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED --add-opens com.azure.digitaltwins.core/com.azure.digitaltwins.core=ALL-UNNAMED diff --git a/sdk/eventgrid/azure-messaging-eventgrid/pom.xml b/sdk/eventgrid/azure-messaging-eventgrid/pom.xml index 830364b7d0f10..080ba1afd509c 100644 --- a/sdk/eventgrid/azure-messaging-eventgrid/pom.xml +++ b/sdk/eventgrid/azure-messaging-eventgrid/pom.xml @@ -141,6 +141,7 @@ 3.0.0-M3 + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED --add-opens com.azure.messaging.eventgrid/com.azure.messaging.eventgrid=ALL-UNNAMED --add-opens com.azure.messaging.eventgrid/com.azure.messaging.eventgrid.implementation=ALL-UNNAMED diff --git a/sdk/monitor/azure-monitor-query/pom.xml b/sdk/monitor/azure-monitor-query/pom.xml index 9c144575d099d..e732e8be79012 100644 --- a/sdk/monitor/azure-monitor-query/pom.xml +++ b/sdk/monitor/azure-monitor-query/pom.xml @@ -27,6 +27,11 @@ https://github.com/Azure/azure-sdk-for-java + + + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED + + com.azure diff --git a/sdk/purview/azure-analytics-purview-catalog/pom.xml b/sdk/purview/azure-analytics-purview-catalog/pom.xml index ef85d5297089e..e0b0658425b1c 100644 --- a/sdk/purview/azure-analytics-purview-catalog/pom.xml +++ b/sdk/purview/azure-analytics-purview-catalog/pom.xml @@ -31,6 +31,9 @@ true + + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED + diff --git a/sdk/purview/azure-analytics-purview-scanning/pom.xml b/sdk/purview/azure-analytics-purview-scanning/pom.xml index 46b39df6b1490..862dc73b94263 100644 --- a/sdk/purview/azure-analytics-purview-scanning/pom.xml +++ b/sdk/purview/azure-analytics-purview-scanning/pom.xml @@ -31,6 +31,9 @@ true + + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED + diff --git a/sdk/search/azure-search-documents/pom.xml b/sdk/search/azure-search-documents/pom.xml index f19d75681dcfd..f46f05219504d 100644 --- a/sdk/search/azure-search-documents/pom.xml +++ b/sdk/search/azure-search-documents/pom.xml @@ -107,6 +107,7 @@ --add-exports com.azure.core/com.azure.core.implementation.http=ALL-UNNAMED + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED --add-opens com.azure.core/com.azure.core.util=ALL-UNNAMED --add-opens com.azure.search.documents/com.azure.search.documents=ALL-UNNAMED --add-opens com.azure.search.documents/com.azure.search.documents.indexes=ALL-UNNAMED diff --git a/sdk/translation/azure-ai-documenttranslator/pom.xml b/sdk/translation/azure-ai-documenttranslator/pom.xml index 7982fb37847cd..c76ba51285d24 100644 --- a/sdk/translation/azure-ai-documenttranslator/pom.xml +++ b/sdk/translation/azure-ai-documenttranslator/pom.xml @@ -32,6 +32,9 @@ true + + --add-exports com.azure.core/com.azure.core.implementation.jackson=ALL-UNNAMED +