Skip to content

Commit

Permalink
Defer Creation of XmlMapper to Allow for Exclusion of Dataformat XML (#…
Browse files Browse the repository at this point in the history
…30663)

Defer Creation of XmlMapper to Allow for Exclusion of Dataformat XML
  • Loading branch information
alzimmermsft authored Sep 14, 2022
1 parent a84a3ef commit b54306d
Show file tree
Hide file tree
Showing 28 changed files with 483 additions and 115 deletions.
6 changes: 6 additions & 0 deletions sdk/core/azure-core-perf/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
<artifactId>perf-test-core</artifactId>
<version>1.0.0-beta.1</version> <!-- {x-version-update;com.azure:perf-test-core;current} -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.3</version> <!-- {x-version-update;com.fasterxml.jackson.dataformat:jackson-dataformat-xml;external_dependency} -->
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-standalone</artifactId>
Expand Down Expand Up @@ -104,6 +109,7 @@
<rules>
<bannedDependencies>
<includes>
<include>com.fasterxml.jackson.dataformat:jackson-dataformat-xml:[2.13.3]</include> <!-- {x-include-update;com.fasterxml.jackson.dataformat:jackson-dataformat-xml;external_dependency} -->
<include>com.github.tomakehurst:wiremock-standalone:[2.24.1]</include> <!-- {x-include-update;com.github.tomakehurst:wiremock-standalone;external_dependency} -->
</includes>
</bannedDependencies>
Expand Down
7 changes: 7 additions & 0 deletions sdk/core/azure-core-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,12 @@
<version>1.33.0-beta.1</version> <!-- {x-version-update;com.azure:azure-core;current} -->
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.3</version> <!-- {x-version-update;com.fasterxml.jackson.dataformat:jackson-dataformat-xml;external_dependency} -->
</dependency>

<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
Expand Down Expand Up @@ -121,6 +127,7 @@
<bannedDependencies>
<includes>
<include>io.projectreactor:reactor-test:[3.4.22]</include> <!-- {x-include-update;io.projectreactor:reactor-test;external_dependency} -->
<include>com.fasterxml.jackson.dataformat:jackson-dataformat-xml:[2.13.3]</include> <!-- {x-include-update;com.fasterxml.jackson.dataformat:jackson-dataformat-xml;external_dependency} -->
<!-- special allowance for azure-core-test as it is not a shipping library: -->
<include>org.junit.jupiter:junit-jupiter-api:[5.8.2]</include> <!-- {x-include-update;org.junit.jupiter:junit-jupiter-api;external_dependency} -->
<include>org.junit.jupiter:junit-jupiter-params:[5.8.2]</include> <!-- {x-include-update;org.junit.jupiter:junit-jupiter-params;external_dependency} -->
Expand Down
1 change: 1 addition & 0 deletions sdk/core/azure-core-test/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
module com.azure.core.test {
requires transitive com.azure.core;

requires com.fasterxml.jackson.dataformat.xml;
requires org.junit.jupiter.api;
requires org.junit.jupiter.params;
requires reactor.test;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@ final class JacksonVersion {
private final String helpString;

private JacksonVersion() {
coreVersion = SemanticVersion.parse(
new com.fasterxml.jackson.core.json.PackageVersion().version().toString());
databindVersion = SemanticVersion.parse(
new com.fasterxml.jackson.databind.cfg.PackageVersion().version().toString());
xmlVersion = SemanticVersion.parse(
new com.fasterxml.jackson.dataformat.xml.PackageVersion().version().toString());
jsr310Version = SemanticVersion.parse(
new com.fasterxml.jackson.datatype.jsr310.PackageVersion().version().toString());
coreVersion = SemanticVersion.parse(com.fasterxml.jackson.core.json.PackageVersion.VERSION.toString());
databindVersion = SemanticVersion.parse(com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION.toString());
jsr310Version = SemanticVersion.parse(com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION.toString());

SemanticVersion xmlVersion1;
try {
Class<?> xmlPackageVersion = Class.forName("com.fasterxml.jackson.dataformat.xml.PackageVersion");
xmlVersion1 = SemanticVersion.parse(xmlPackageVersion.getDeclaredField("VERSION").get(null).toString());
} catch (ReflectiveOperationException e) {
xmlVersion1 = SemanticVersion.createInvalid();
}
xmlVersion = xmlVersion1;

checkVersion(coreVersion, CORE_PACKAGE_NAME);
checkVersion(databindVersion, DATABIND_PACKAGE_NAME);
checkVersion(xmlVersion, XML_PACKAGE_NAME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,64 +13,17 @@
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.cfg.MapperBuilder;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
* Constructs and configures {@link ObjectMapper} instances.
*/
final class ObjectMapperFactory {
// ObjectMapperFactory is a commonly used factory, use a static logger.
private static final ClientLogger LOGGER = new ClientLogger(ObjectMapperFactory.class);

private static final String MUTABLE_COERCION_CONFIG = "com.fasterxml.jackson.databind.cfg.MutableCoercionConfig";
private static final String COERCION_INPUT_SHAPE = "com.fasterxml.jackson.databind.cfg.CoercionInputShape";
private static final String COERCION_ACTION = "com.fasterxml.jackson.databind.cfg.CoercionAction";

private MethodHandle coercionConfigDefaults;
private MethodHandle setCoercion;
private Object coercionInputShapeEmptyString;
private Object coercionActionAsNull;
private boolean useReflectionToSetCoercion;

public static final ObjectMapperFactory INSTANCE = new ObjectMapperFactory();

private ObjectMapperFactory() {
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

try {
Class<?> mutableCoercionConfig = Class.forName(MUTABLE_COERCION_CONFIG);
Class<?> coercionInputShapeClass = Class.forName(COERCION_INPUT_SHAPE);
Class<?> coercionActionClass = Class.forName(COERCION_ACTION);

coercionConfigDefaults = publicLookup.findVirtual(ObjectMapper.class, "coercionConfigDefaults",
MethodType.methodType(mutableCoercionConfig));
setCoercion = publicLookup.findVirtual(mutableCoercionConfig, "setCoercion",
MethodType.methodType(mutableCoercionConfig, coercionInputShapeClass, coercionActionClass));
coercionInputShapeEmptyString = publicLookup.findStaticGetter(coercionInputShapeClass, "EmptyString",
coercionInputShapeClass).invoke();
coercionActionAsNull = publicLookup.findStaticGetter(coercionActionClass, "AsNull", coercionActionClass)
.invoke();
useReflectionToSetCoercion = true;
} catch (Throwable ex) {
// Throw the Error only if it isn't a LinkageError.
// This initialization is attempting to use classes that may not exist.
if (ex instanceof Error && !(ex instanceof LinkageError)) {
throw (Error) ex;
}

LOGGER.verbose("Failed to retrieve MethodHandles used to set coercion configurations. "
+ "Setting coercion configurations will be skipped. "
+ "Please update your Jackson dependencies to at least version 2.12", ex);
}
}

public ObjectMapper createJsonMapper(ObjectMapper innerMapper) {
ObjectMapper flatteningMapper = initializeMapperBuilder(JsonMapper.builder())
.addModule(FlatteningSerializer.getModule(innerMapper))
Expand All @@ -87,32 +40,7 @@ public ObjectMapper createJsonMapper(ObjectMapper innerMapper) {
}

public ObjectMapper createXmlMapper() {
ObjectMapper xmlMapper = initializeMapperBuilder(XmlMapper.builder())
.defaultUseWrapper(false)
.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION)
/*
* In Jackson 2.12 the default value of this feature changed from true to false.
* https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.12#xml-module
*/
.enable(FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL)
.build();

if (useReflectionToSetCoercion) {
try {
Object object = coercionConfigDefaults.invoke(xmlMapper);
setCoercion.invoke(object, coercionInputShapeEmptyString, coercionActionAsNull);
} catch (Throwable e) {
if (e instanceof Error) {
throw (Error) e;
}

LOGGER.verbose("Failed to set coercion actions.", e);
}
} else {
LOGGER.verbose("Didn't set coercion defaults as it wasn't found on the classpath.");
}

return xmlMapper;
return XmlMapperFactory.INSTANCE.createXmlMapper();
}

public ObjectMapper createSimpleMapper() {
Expand All @@ -134,7 +62,7 @@ public ObjectMapper createHeaderMapper() {
}

@SuppressWarnings("deprecation")
private <S extends MapperBuilder<?, ?>> S initializeMapperBuilder(S mapper) {
static <S extends MapperBuilder<?, ?>> S initializeMapperBuilder(S mapper) {
mapper.enable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS)
.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.core.implementation.jackson;

import com.azure.core.util.logging.ClientLogger;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public final class XmlMapperFactory {
private static final ClientLogger LOGGER = new ClientLogger(XmlMapperFactory.class);

private static final String MUTABLE_COERCION_CONFIG = "com.fasterxml.jackson.databind.cfg.MutableCoercionConfig";
private static final String COERCION_INPUT_SHAPE = "com.fasterxml.jackson.databind.cfg.CoercionInputShape";
private static final String COERCION_ACTION = "com.fasterxml.jackson.databind.cfg.CoercionAction";

private MethodHandle coercionConfigDefaults;
private MethodHandle setCoercion;
private Object coercionInputShapeEmptyString;
private Object coercionActionAsNull;
private boolean useReflectionToSetCoercion;

public static final XmlMapperFactory INSTANCE = new XmlMapperFactory();

private XmlMapperFactory() {
MethodHandles.Lookup publicLookup = MethodHandles.publicLookup();

try {
Class<?> mutableCoercionConfig = Class.forName(MUTABLE_COERCION_CONFIG);
Class<?> coercionInputShapeClass = Class.forName(COERCION_INPUT_SHAPE);
Class<?> coercionActionClass = Class.forName(COERCION_ACTION);

coercionConfigDefaults = publicLookup.findVirtual(ObjectMapper.class, "coercionConfigDefaults",
MethodType.methodType(mutableCoercionConfig));
setCoercion = publicLookup.findVirtual(mutableCoercionConfig, "setCoercion",
MethodType.methodType(mutableCoercionConfig, coercionInputShapeClass, coercionActionClass));
coercionInputShapeEmptyString = publicLookup.findStaticGetter(coercionInputShapeClass, "EmptyString",
coercionInputShapeClass).invoke();
coercionActionAsNull = publicLookup.findStaticGetter(coercionActionClass, "AsNull", coercionActionClass)
.invoke();
useReflectionToSetCoercion = true;
} catch (Throwable ex) {
// Throw the Error only if it isn't a LinkageError.
// This initialization is attempting to use classes that may not exist.
if (ex instanceof Error && !(ex instanceof LinkageError)) {
throw (Error) ex;
}

LOGGER.verbose("Failed to retrieve MethodHandles used to set coercion configurations. "
+ "Setting coercion configurations will be skipped. "
+ "Please update your Jackson dependencies to at least version 2.12", ex);
}
}

public ObjectMapper createXmlMapper() {
ObjectMapper xmlMapper = ObjectMapperFactory.initializeMapperBuilder(XmlMapper.builder())
.defaultUseWrapper(false)
.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION)
/*
* In Jackson 2.12 the default value of this feature changed from true to false.
* https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.12#xml-module
*/
.enable(FromXmlParser.Feature.EMPTY_ELEMENT_AS_NULL)
.build();

if (useReflectionToSetCoercion) {
try {
Object object = coercionConfigDefaults.invoke(xmlMapper);
setCoercion.invoke(object, coercionInputShapeEmptyString, coercionActionAsNull);
} catch (Throwable e) {
if (e instanceof Error) {
throw (Error) e;
}

LOGGER.verbose("Failed to set coercion actions.", e);
}
} else {
LOGGER.verbose("Didn't set coercion defaults as it wasn't found on the classpath.");
}

return xmlMapper;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@ public class JacksonAdapter implements SerializerAdapter {
* An instance of {@link ObjectMapperShim} to serialize/deserialize objects.
*/
private final ObjectMapperShim mapper;

private final ObjectMapperShim xmlMapper;

private final ObjectMapperShim headerMapper;

private volatile ObjectMapperShim xmlMapper;

/**
* Raw mappers are needed only to support deprecated simpleMapper() and serializer().
*/
Expand All @@ -63,11 +62,11 @@ public JacksonAdapter() {
/**
* Creates a new JacksonAdapter instance with Azure Core mapper settings and applies additional configuration
* through {@code configureSerialization} callback.
*
* <p>
* {@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.
*
* <p>
* Register modules on the outer instance to add custom (de)serializers similar to {@code new JacksonAdapter((outer,
* inner) -> outer.registerModule(new MyModule()))}
*
Expand All @@ -79,7 +78,6 @@ public JacksonAdapter() {
public JacksonAdapter(BiConsumer<ObjectMapper, ObjectMapper> configureSerialization) {
Objects.requireNonNull(configureSerialization, "'configureSerialization' cannot be null.");
this.headerMapper = ObjectMapperShim.createHeaderMapper();
this.xmlMapper = ObjectMapperShim.createXmlMapper();
this.mapper = ObjectMapperShim.createJsonMapper(ObjectMapperShim.createSimpleMapper(),
(outerMapper, innerMapper) -> captureRawMappersAndConfigure(outerMapper, innerMapper, configureSerialization));
}
Expand Down Expand Up @@ -140,7 +138,7 @@ public String serialize(Object object, SerializerEncoding encoding) throws IOExc
}

return (String) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
? xmlMapper.writeValueAsString(object)
? getXmlMapper().writeValueAsString(object)
: mapper.writeValueAsString(object));
}

Expand All @@ -151,7 +149,7 @@ public byte[] serializeToBytes(Object object, SerializerEncoding encoding) throw
}

return (byte[]) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
? xmlMapper.writeValueAsBytes(object)
? getXmlMapper().writeValueAsBytes(object)
: mapper.writeValueAsBytes(object));
}

Expand All @@ -163,7 +161,7 @@ public void serialize(Object object, SerializerEncoding encoding, OutputStream o

useAccessHelper(() -> {
if (encoding == SerializerEncoding.XML) {
xmlMapper.writeValue(outputStream, object);
getXmlMapper().writeValue(outputStream, object);
} else {
mapper.writeValue(outputStream, object);
}
Expand Down Expand Up @@ -209,7 +207,7 @@ public <T> T deserialize(String value, Type type, SerializerEncoding encoding) t
}

return (T) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
? xmlMapper.readValue(value, type)
? getXmlMapper().readValue(value, type)
: mapper.readValue(value, type));
}

Expand All @@ -221,7 +219,7 @@ public <T> T deserialize(byte[] bytes, Type type, SerializerEncoding encoding) t
}

return (T) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
? xmlMapper.readValue(bytes, type)
? getXmlMapper().readValue(bytes, type)
: mapper.readValue(bytes, type));
}

Expand All @@ -234,7 +232,7 @@ public <T> T deserialize(InputStream inputStream, final Type type, SerializerEnc
}

return (T) useAccessHelper(() -> (encoding == SerializerEncoding.XML)
? xmlMapper.readValue(inputStream, type)
? getXmlMapper().readValue(inputStream, type)
: mapper.readValue(inputStream, type));
}

Expand All @@ -250,6 +248,18 @@ public <T> T deserializeHeader(Header header, Type type) throws IOException {
return (T) useAccessHelper(() -> headerMapper.readValue(header.getValue(), type));
}

private ObjectMapperShim getXmlMapper() {
if (xmlMapper == null) {
synchronized (mapper) {
if (xmlMapper == null) {
xmlMapper = ObjectMapperShim.createXmlMapper();
}
}
}

return xmlMapper;
}

@SuppressWarnings("removal")
private static Object useAccessHelper(IOExceptionCallable serializationCall) throws IOException {
if (useAccessHelper) {
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/azure-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
requires transitive com.fasterxml.jackson.core;
requires transitive com.fasterxml.jackson.databind;

requires transitive com.fasterxml.jackson.dataformat.xml;
requires com.fasterxml.jackson.dataformat.xml;
requires transitive com.fasterxml.jackson.datatype.jsr310;

// public API surface area
Expand Down
Loading

0 comments on commit b54306d

Please sign in to comment.