XmlMapper.java
package tools.jackson.dataformat.xml;
import java.io.Closeable;
import java.io.IOException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.core.*;
import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.*;
import tools.jackson.databind.cfg.CoercionAction;
import tools.jackson.databind.cfg.CoercionInputShape;
import tools.jackson.databind.cfg.DeserializationContexts;
import tools.jackson.databind.cfg.MapperBuilder;
import tools.jackson.databind.cfg.MapperBuilderState;
import tools.jackson.databind.cfg.SerializationContexts;
import tools.jackson.databind.jsontype.PolymorphicTypeValidator;
import tools.jackson.databind.jsontype.TypeResolverBuilder;
import tools.jackson.databind.ser.SerializationContextExt;
import tools.jackson.databind.type.LogicalType;
import tools.jackson.dataformat.xml.deser.FromXmlParser;
import tools.jackson.dataformat.xml.deser.XmlDeserializationContexts;
import tools.jackson.dataformat.xml.ser.ToXmlGenerator;
import tools.jackson.dataformat.xml.ser.XmlSerializationContexts;
import tools.jackson.dataformat.xml.util.DefaultXmlPrettyPrinter;
/**
* Customized {@link ObjectMapper} that will read and write XML instead of JSON,
* using XML-backed {@link tools.jackson.core.TokenStreamFactory}
* implementation ({@link XmlFactory}), operation on STAX
* {@link javax.xml.stream.XMLStreamReader}s and
* {@link javax.xml.stream.XMLStreamWriter}s.
*<p>
* Mapper itself overrides some aspects of functionality to try to handle
* data binding aspects as similar to JAXB as possible.
*/
public class XmlMapper extends ObjectMapper
{
private static final long serialVersionUID = 1L;
protected final static DefaultXmlPrettyPrinter DEFAULT_XML_PRETTY_PRINTER = new DefaultXmlPrettyPrinter();
/**
* Builder implementation for constructing {@link XmlMapper} instances.
*
* @since 3.0
*/
public static class Builder extends MapperBuilder<XmlMapper, Builder>
{
protected boolean _defaultUseWrapper;
protected String _nameForTextElement;
/*
/******************************************************************
/* Life-cycle
/******************************************************************
*/
public Builder(XmlFactory f) {
super(f);
// 21-Jun-2017, tatu: Seems like there are many cases in XML where ability to coerce empty
// String into `null` (where it otherwise is an error) is very useful.
enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
_defaultUseWrapper = JacksonXmlAnnotationIntrospector.DEFAULT_USE_WRAPPER;
_nameForTextElement = FromXmlParser.DEFAULT_UNNAMED_TEXT_PROPERTY;
// as well as AnnotationIntrospector: note, however, that "use wrapper" may well
// change later on
annotationIntrospector(new JacksonXmlAnnotationIntrospector(_defaultUseWrapper));
// Need to mangle Type Ids (to avoid invalid XML Name characters); one way is this:
typeResolverProvider(new XmlTypeResolverProvider());
// Some changes easiest to apply via Module
addModule(new XmlModule());
// 04-May-2018, tatu: Important! Let's also default `String` `null` handling to coerce
// to empty string -- this lets us induce `null` from empty tags firs
// 08-Sep-2019, tatu: This causes [dataformat-xml#359] (follow-up for #354), but
// can not simply be removed.
// 19-Mar-2023, tatu: Nah. Let's not coerce to let `xsi:nil` works its magic
//_configOverrides.findOrCreateOverride(String.class)
// .setNullHandling(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY));
// 13-May-2020, tatu: [dataformat-xml#377] Need to ensure we will keep XML-specific
// Base64 default as "MIME" (not MIME-NO-LINEFEEDS), to preserve pre-2.12
// behavior
defaultBase64Variant(Base64Variants.MIME);
// 04-Jun-2020, tatu: Use new (2.12) "CoercionConfigs" to support coercion
// from empty and blank Strings to "empty" POJOs etc
_coercionConfigs.defaultCoercions()
// To allow indentation without problems, need to accept blank String as empty:
.setAcceptBlankAsEmpty(Boolean.TRUE)
// and then coercion from empty String to empty value, in general
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsEmpty)
;
// 03-May-2021, tatu: ... except make sure to keep "empty to Null" for
// scalar types...
_coercionConfigs.findOrCreateCoercion(LogicalType.Integer)
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
_coercionConfigs.findOrCreateCoercion(LogicalType.Float)
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
_coercionConfigs.findOrCreateCoercion(LogicalType.Boolean)
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.AsNull);
}
@Override
public XmlMapper build() {
return new XmlMapper(this);
}
@Override
protected MapperBuilderState _saveState() {
return new XmlBuilderState(this);
}
/**
* NOTE: despite being public not exposed as part of API
*
* @param state State to restore builder from
*/
@SuppressWarnings("exports")
public Builder(XmlBuilderState state) {
super(state);
_defaultUseWrapper = state._defaultUseWrapper;
_nameForTextElement = state._nameForTextElement;
}
/*
/******************************************************************
/* Default value overrides
/******************************************************************
*/
@Override
protected SerializationContexts _defaultSerializationContexts() {
return new XmlSerializationContexts();
}
@Override
protected DeserializationContexts _defaultDeserializationContexts() {
return new XmlDeserializationContexts();
}
/**
* Overridden to (try to) ensure we use XML-compatible default indenter
*/
@Override
protected PrettyPrinter _defaultPrettyPrinter() {
return DEFAULT_XML_PRETTY_PRINTER;
}
/*
/******************************************************************
/* Overrides for polymorphic type handling
/******************************************************************
*/
@Override
protected TypeResolverBuilder<?> _defaultDefaultTypingResolver(PolymorphicTypeValidator ptv,
DefaultTyping applicability,
JsonTypeInfo.As includeAs) {
return new DefaultingXmlTypeResolverBuilder(ptv, applicability, includeAs);
}
@Override
protected TypeResolverBuilder<?> _defaultDefaultTypingResolver(PolymorphicTypeValidator ptv,
DefaultTyping applicability,
String propertyName) {
return new DefaultingXmlTypeResolverBuilder(ptv, applicability, propertyName);
}
// Since WRAPPER_ARRAY does not work well, map to WRAPPER_OBJECT
@Override
public Builder activateDefaultTyping(PolymorphicTypeValidator ptv, DefaultTyping dti) {
return activateDefaultTyping(ptv, dti, JsonTypeInfo.As.WRAPPER_OBJECT);
}
/*
/******************************************************************
/* XML format features
/******************************************************************
*/
public Builder enable(XmlReadFeature... features) {
for (XmlReadFeature f : features) {
_formatReadFeatures |= f.getMask();
}
return this;
}
public Builder disable(XmlReadFeature... features) {
for (XmlReadFeature f : features) {
_formatReadFeatures &= ~f.getMask();
}
return this;
}
public Builder configure(XmlReadFeature feature, boolean state)
{
if (state) {
_formatReadFeatures |= feature.getMask();
} else {
_formatReadFeatures &= ~feature.getMask();
}
return this;
}
public Builder enable(XmlWriteFeature... features) {
for (XmlWriteFeature f : features) {
_formatWriteFeatures |= f.getMask();
}
return this;
}
public Builder disable(XmlWriteFeature... features) {
for (XmlWriteFeature f : features) {
_formatWriteFeatures &= ~f.getMask();
}
return this;
}
public Builder configure(XmlWriteFeature feature, boolean state)
{
if (state) {
_formatWriteFeatures |= feature.getMask();
} else {
_formatWriteFeatures &= ~feature.getMask();
}
return this;
}
/**
* The builder returned uses default settings more closely
* matching the default configs used in Jackson 2.x versions.
* <p>
* This method is still a work in progress and may not yet fully replicate the
* default settings of Jackson 2.x.
* </p>
*/
@Override
public Builder configureForJackson2() {
return super.configureForJackson2()
.disable(XmlWriteFeature.WRITE_NULLS_AS_XSI_NIL)
.disable(XmlWriteFeature.UNWRAP_ROOT_OBJECT_NODE)
.disable(XmlWriteFeature.AUTO_DETECT_XSI_TYPE)
.disable(XmlWriteFeature.WRITE_XML_SCHEMA_CONFORMING_FLOATS)
.disable(XmlReadFeature.AUTO_DETECT_XSI_TYPE);
}
/*
/******************************************************************
/* XML specific additional config
/******************************************************************
*/
public boolean defaultUseWrapper() {
return _defaultUseWrapper;
}
/**
* Determination of whether indexed properties (arrays, Lists) that are not explicitly
* annotated (with {@link tools.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper}
* or equivalent) should default to using implicit wrapper (with same name as property) or not.
* If enabled, wrapping is used by default; if false, it is not.
*<p>
* Note that JAXB annotation introspector always assumes "do not wrap by default".
* Jackson annotations have different default due to backwards compatibility.
*/
public Builder defaultUseWrapper(boolean b) {
if (_defaultUseWrapper != b) {
_defaultUseWrapper = b;
AnnotationIntrospector ai0 = annotationIntrospector();
for (AnnotationIntrospector ai : ai0.allIntrospectors()) {
if (ai instanceof JacksonXmlAnnotationIntrospector) {
((JacksonXmlAnnotationIntrospector) ai).setDefaultUseWrapper(b);
}
}
}
return this;
}
public String nameForTextElement() {
return _nameForTextElement;
}
/**
* Name used for pseudo-property used for returning XML Text value (cdata within
* element, which does not have actual element name to use) as a named value (since
* JSON data model just has named values, except for arrays).
* Defaults to empty String, but may be changed for interoperability reasons:
* JAXB, for example, uses "value" as name.
*/
public Builder nameForTextElement(String elem) {
if (elem == null) {
elem = "";
}
_nameForTextElement = elem;
_streamFactory = ((XmlFactory) _streamFactory).withNameForTextElement(elem);
return this;
}
}
/**
* Saved configuration entity to use with builder for {@link XmlMapper} instances.
*
* @since 3.0
*/
protected static class XmlBuilderState extends MapperBuilderState
implements java.io.Serializable // important!
{
private static final long serialVersionUID = 3L;
protected final boolean _defaultUseWrapper;
protected final String _nameForTextElement;
public XmlBuilderState(Builder src) {
super(src);
_defaultUseWrapper = src._defaultUseWrapper;
_nameForTextElement = src._nameForTextElement;
}
// We also need actual instance of state as base class can not implement logic
// for reinstating mapper (via mapper builder) from state.
@Override
protected Object readResolve() {
return new Builder(this).build();
}
}
/*
/**********************************************************************
/* Life-cycle: construction 3.0 style
/**********************************************************************
*/
public XmlMapper(Builder b)
{
super(b);
}
public static XmlMapper.Builder xmlBuilder() {
return new XmlMapper.Builder(new XmlFactory());
}
public static XmlMapper.Builder builder() {
return new XmlMapper.Builder(new XmlFactory());
}
public static XmlMapper.Builder builder(XmlFactory streamFactory) {
return new XmlMapper.Builder(streamFactory);
}
/**
* The builder returned uses default settings more closely
* matching the default configs used in Jackson 2.x versions.
* <p>
* This method is still a work in progress and may not yet fully replicate the
* default settings of Jackson 2.x.
* </p>
*/
public static XmlMapper.Builder builderWithJackson2Defaults() {
return builder(XmlFactory.builderWithJackson2Defaults().build())
.configureForJackson2();
}
@SuppressWarnings("unchecked")
@Override
public XmlMapper.Builder rebuild() {
return new XmlMapper.Builder((XmlBuilderState) _savedBuilderState);
}
/*
/**********************************************************************
/* Life-cycle: JDK serialization support
/**********************************************************************
*/
// 27-Feb-2018, tatu: Not sure why but it seems base class definitions
// are not sufficient alone; sub-classes must re-define.
@Override
protected Object writeReplace() {
return _savedBuilderState;
}
@Override
protected Object readResolve() {
throw new IllegalStateException("Should never deserialize `"+getClass().getName()+"` directly");
}
/*
/**********************************************************************
/* Life-cycle: construction, legacy
/**********************************************************************
*/
public XmlMapper() {
this(new XmlFactory());
}
public XmlMapper(XmlFactory xmlFactory)
{
this(new Builder(xmlFactory));
/*
// Need to override serializer provider (due to root name handling);
// deserializer provider fine as is
super(xmlFactory, new XmlSerializationContext(xmlFactory, new XmlRootNameLookup()), null);
_xmlModule = module;
// but all the rest is done via Module interface!
if (module != null) {
registerModule(module);
}
// 19-May-2015, tatu: Must ensure we use XML-specific indenter
_serializationConfig = _serializationConfig.withDefaultPrettyPrinter(DEFAULT_XML_PRETTY_PRINTER);
// 21-Jun-2017, tatu: Seems like there are many cases in XML where ability to coerce empty
// String into `null` (where it otherwise is an error) is very useful.
enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
*/
}
/*
/**********************************************************************
/* Life-cycle, shared "vanilla" (default configuration) instance
/**********************************************************************
*/
/**
* Accessor method for getting globally shared "default" {@link XmlMapper}
* instance: one that has default configuration, no (custom) modules registered, no
* config overrides. Usable mostly when dealing "untyped" or Tree-style
* content reading and writing.
*/
public static XmlMapper shared() {
return SharedWrapper.wrapped();
}
/*
/**********************************************************************
/* Access to configuration settings
/**********************************************************************
*/
@Override
public Version version() {
return PackageVersion.VERSION;
}
@Override
public XmlFactory tokenStreamFactory() {
return (XmlFactory) _streamFactory;
}
/*
/**********************************************************************
/* Format-specific
/**********************************************************************
*/
public boolean isEnabled(XmlReadFeature f) {
return _deserializationConfig.hasFormatFeature(f);
}
public boolean isEnabled(XmlWriteFeature f) {
return _serializationConfig.hasFormatFeature(f);
}
/*
/**********************************************************************
/* XML-specific access
/**********************************************************************
*/
/**
* Overloaded variant that allows constructing {@link FromXmlParser}
* for given Stax {@link XMLStreamReader}.
*/
public FromXmlParser createParser(XMLStreamReader r) throws IOException {
DeserializationContext ctxt = _deserializationContext();
return tokenStreamFactory().createParser(ctxt, r);
}
/**
* Overloaded variant that allows constructing {@link ToXmlGenerator}
* for given Stax {@link XMLStreamWriter}.
*/
public ToXmlGenerator createGenerator(XMLStreamWriter w) throws IOException {
SerializationContextExt prov = _serializationContext(serializationConfig());
return tokenStreamFactory().createGenerator(prov, w);
}
/**
* Method for reading a single XML value from given XML-specific input
* source; useful for incremental data-binding, combining traversal using
* basic Stax {@link XMLStreamReader} with data-binding by Jackson.
*/
public <T> T readValue(XMLStreamReader r, Class<T> valueType) throws IOException {
return readValue(r, _typeFactory.constructType(valueType));
}
/**
* Method for reading a single XML value from given XML-specific input
* source; useful for incremental data-binding, combining traversal using
* basic Stax {@link XMLStreamReader} with data-binding by Jackson.
*/
public <T> T readValue(XMLStreamReader r, TypeReference<T> valueTypeRef) throws IOException {
return readValue(r, _typeFactory.constructType(valueTypeRef));
}
/**
* Method for reading a single XML value from given XML-specific input
* source; useful for incremental data-binding, combining traversal using
* basic Stax {@link XMLStreamReader} with data-binding by Jackson.
*/
@SuppressWarnings("resource")
public <T> T readValue(XMLStreamReader r, JavaType valueType) throws IOException
{
return super.readValue(createParser(r), valueType);
}
/**
* Method for serializing given value using specific {@link XMLStreamReader}:
* useful when building large XML files by binding individual items, one at
* a time.
*/
@SuppressWarnings("resource")
public void writeValue(XMLStreamWriter w, Object value) throws IOException
{
// 04-Oct-2017, tatu: Unfortunately can not simply delegate to super-class implementation
// because we need the context first...
ToXmlGenerator g = createGenerator(w);
final SerializationConfig config = serializationConfig();
if (config.isEnabled(SerializationFeature.CLOSE_CLOSEABLE) && (value instanceof Closeable)) {
_writeCloseableValue(g, value, config);
} else {
_serializationContext(config).serializeValue(g, value);
if (config.isEnabled(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)) {
g.flush();
}
}
}
/*
/**********************************************************
/* Helper class(es)
/**********************************************************
*/
/**
* Helper class to contain dynamically constructed "shared" instance of
* mapper, should one be needed via {@link #shared}.
*/
private final static class SharedWrapper {
private final static XmlMapper MAPPER = XmlMapper.builder().build();
public static XmlMapper wrapped() { return MAPPER; }
}
}