XmlBeanSerializerBase.java
package tools.jackson.dataformat.xml.ser;
import java.util.BitSet;
import java.util.Set;
import javax.xml.namespace.QName;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import tools.jackson.core.*;
import tools.jackson.databind.DatabindException;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.jsontype.TypeSerializer;
import tools.jackson.databind.ser.BeanPropertyWriter;
import tools.jackson.databind.ser.PropertyFilter;
import tools.jackson.databind.ser.WritableObjectId;
import tools.jackson.databind.ser.bean.BeanSerializerBase;
import tools.jackson.databind.ser.impl.ObjectIdWriter;
import tools.jackson.databind.util.NameTransformer;
import tools.jackson.dataformat.xml.util.XmlInfo;
/**
* Specific sub-class of {@link BeanSerializerBase} needed to take care
* of some xml-specific aspects, such as distinction between attributes
* and elements.
*/
public abstract class XmlBeanSerializerBase extends BeanSerializerBase
{
/**
* Marker used for storing associated internal data with {@link BeanPropertyWriter}
* instances; to mark instances that are to be written out as attributes.
* Created as separate non-interned String to ensure there are no collisions.
*/
public final static String KEY_XML_INFO = new String("xmlInfo");
/**
* Number of attributes to write; these will have been ordered to be the first
* properties to write.
*/
protected final int _attributeCount;
/**
* Index of "text value" property we have, if any; can have at most
* one such property.
*/
protected final int _textPropertyIndex;
/**
* Array that contains namespace URIs associated with properties, if any;
* null if no namespace definitions have been assigned
*/
protected final QName[] _xmlNames;
/**
* Optional set of indexes of properties that should be serialized as CDATA,
* instead of regular XML text segment. Left as null in cases where none of
* element values are to be written in such a way.
*/
protected final BitSet _cdata;
public XmlBeanSerializerBase(BeanSerializerBase src)
{
super(src);
XmlInfoInternal info = _resolveXmlInfo(_props, _filteredProps);
_attributeCount = info.attributeCount;
_textPropertyIndex = info.textPropertyIndex;
_xmlNames = info.xmlNames;
_cdata = info.cdata;
}
protected XmlBeanSerializerBase(XmlBeanSerializerBase src, ObjectIdWriter objectIdWriter)
{
super(src, objectIdWriter);
_attributeCount = src._attributeCount;
_textPropertyIndex = src._textPropertyIndex;
_xmlNames = src._xmlNames;
_cdata = src._cdata;
}
protected XmlBeanSerializerBase(XmlBeanSerializerBase src, ObjectIdWriter objectIdWriter, Object filterId)
{
super(src, objectIdWriter, filterId);
_attributeCount = src._attributeCount;
_textPropertyIndex = src._textPropertyIndex;
_xmlNames = src._xmlNames;
_cdata = src._cdata;
}
protected XmlBeanSerializerBase(XmlBeanSerializerBase src,
Set<String> toIgnore, Set<String> toInclude)
{
super(src, toIgnore, toInclude);
// 13-Sep-2026, tatu: `super(...)` drops the ignored/non-included writers and
// re-indexes `_props`, so the per-index XML metadata can not be copied
// from `src`; recompute it against the new property array.
XmlInfoInternal info = _resolveXmlInfo(_props, _filteredProps);
_attributeCount = info.attributeCount;
_textPropertyIndex = info.textPropertyIndex;
_xmlNames = info.xmlNames;
_cdata = info.cdata;
}
public XmlBeanSerializerBase(XmlBeanSerializerBase src, NameTransformer transformer)
{
super(src, transformer);
// `super(...)` renames writers (count and order unchanged), so copying
// from `src` would leave stale local names in `_xmlNames`; recompute for
// consistency with the other rebuilding constructors. Attribute
// re-ordering is a no-op here since `src` arrays are already ordered.
XmlInfoInternal info = _resolveXmlInfo(_props, _filteredProps);
_attributeCount = info.attributeCount;
_textPropertyIndex = info.textPropertyIndex;
_xmlNames = info.xmlNames;
_cdata = info.cdata;
}
protected XmlBeanSerializerBase(XmlBeanSerializerBase src,
BeanPropertyWriter[] properties, BeanPropertyWriter[] filteredProperties) {
super(src, properties, filteredProperties);
// A modifier may hand us a different (re-ordered or filtered) set of
// writers, so the per-index XML metadata has to be recomputed to stay
// aligned rather than copied from `src`.
XmlInfoInternal info = _resolveXmlInfo(_props, _filteredProps);
_attributeCount = info.attributeCount;
_textPropertyIndex = info.textPropertyIndex;
_xmlNames = info.xmlNames;
_cdata = info.cdata;
}
/**
* Derives the XML-specific, index-parallel metadata from the given property
* arrays.
*<p>
* NOTE: mutates the given arrays. If any attribute is found, attributes are
* re-ordered to the front of {@code props} -- and, in lock-step, of
* {@code filteredProps} -- matching prior behavior. Since
* {@link BeanSerializerBase} keeps the arrays passed to its
* {@code (src, properties, filteredProperties)} constructor as-is, this
* re-ordering is visible to callers of {@code withProperties(...)}: a caller
* that keeps using the arrays it handed in should pass copies.
*
* @param props Properties to derive metadata from; re-ordered in place
* @param filteredProps View-filtered counterpart of {@code props} (entries
* may be {@code null}), kept in the same order; may be {@code null}, but
* if not, must be at least as long as {@code props}
*
* @since 3.3
*/
private static XmlInfoInternal _resolveXmlInfo(BeanPropertyWriter[] props,
BeanPropertyWriter[] filteredProps)
{
// Then make sure attributes are sorted before elements, keep track
// of how many there are altogether
int attrCount = 0;
for (BeanPropertyWriter bpw : props) {
if (_isAttribute(bpw)) { // Yup: let's build re-ordered list then
attrCount = _orderAttributesFirst(props, filteredProps);
break;
}
}
// also: pre-compute need, if any, for CDATA handling:
BitSet cdata = null;
for (int i = 0, len = props.length; i < len; ++i) {
BeanPropertyWriter bpw = props[i];
if (_isCData(bpw)) {
if (cdata == null) {
cdata = new BitSet(len);
}
cdata.set(i);
}
}
// And then collect namespace information
QName[] xmlNames = new QName[props.length];
int textIndex = -1;
for (int i = 0, len = props.length; i < len; ++i) {
BeanPropertyWriter bpw = props[i];
XmlInfo info = (XmlInfo) bpw.getInternalSetting(KEY_XML_INFO);
String ns = null;
if (info != null) {
ns = info.getNamespace();
if (textIndex < 0 && info.isText()) {
textIndex = i;
}
}
xmlNames[i] = new QName((ns == null) ? "" : ns, bpw.getName());
}
return new XmlInfoInternal(attrCount, textIndex, xmlNames, cdata);
}
/*
/**********************************************************************
/* Overridden serialization methods
/**********************************************************************
*/
/**
* Main serialization method needs to be overridden to allow XML-specific
* extra handling, such as indication of whether to write attributes or
* elements.
*/
@Override
protected void _serializeProperties(Object bean, JsonGenerator gen0, SerializationContext ctxt)
throws JacksonException
{
// 19-Aug-2013, tatu: During 'convertValue()', need to skip
if (!(gen0 instanceof ToXmlGenerator xgen)) {
super._serializeProperties(bean, gen0, ctxt);
return;
}
final BeanPropertyWriter[] props;
if (_filteredProps != null && ctxt.getActiveView() != null) {
props = _filteredProps;
} else {
props = _props;
}
final int attrCount = _attributeCount;
final boolean isAttribute = xgen._nextIsAttribute;
if (attrCount > 0) {
xgen.setNextIsAttribute(true);
}
final int textIndex = _textPropertyIndex;
final QName[] xmlNames = _xmlNames;
int i = 0;
final BitSet cdata = _cdata;
try {
for (final int len = props.length; i < len; ++i) {
// 28-jan-2014, pascal: we don't want to reset the attribute flag if we are an unwrapping serializer
// that started with nextIsAttribute to true because all properties should be unwrapped as attributes too.
if (i == attrCount && !(isAttribute && isUnwrappingSerializer())) {
xgen.setNextIsAttribute(false);
}
// also: if this is property to write as text ("unwrap"), need to:
if (i == textIndex) {
xgen.setNextIsUnwrapped(true);
}
xgen.setNextName(xmlNames[i]);
BeanPropertyWriter prop = props[i];
if (prop != null) { // can have nulls in filtered list
if ((cdata != null) && cdata.get(i)) {
xgen.setNextIsCData(true);
prop.serializeAsProperty(bean, xgen, ctxt);
xgen.setNextIsCData(false);
} else {
prop.serializeAsProperty(bean, xgen, ctxt);
}
}
// Reset to avoid next value being written as unwrapped,
// for example when property is suppressed
if (i == textIndex) {
xgen.setNextIsUnwrapped(false);
}
}
} catch (Exception e) {
String name = (i == props.length) ? "[anySetter]" : props[i].getName();
wrapAndThrow(ctxt, e, bean, name);
} catch (StackOverflowError e) { // Bit tricky, can't do more calls as stack is full; so:
final String name = (i == props.length) ? "[anySetter]" : props[i].getName();
throw DatabindException.from(gen0, "Infinite recursion (StackOverflowError)")
.prependPath(bean, name);
}
}
@Override
protected void _serializePropertiesFiltered(Object bean, JsonGenerator gen0,
SerializationContext ctxt, Object filterId)
throws JacksonException
{
// 19-Aug-2013, tatu: During 'convertValue()', need to skip
if (!(gen0 instanceof ToXmlGenerator xgen)) {
super._serializePropertiesFiltered(bean, gen0, ctxt, filterId);
return;
}
final BeanPropertyWriter[] props;
if (_filteredProps != null && ctxt.getActiveView() != null) {
props = _filteredProps;
} else {
props = _props;
}
final PropertyFilter filter = findPropertyFilter(ctxt, _propertyFilterId, bean);
// better also allow missing filter actually..
if (filter == null) {
_serializeProperties(bean, gen0, ctxt);
return;
}
final boolean isAttribute = xgen._nextIsAttribute;
final int attrCount = _attributeCount;
if (attrCount > 0) {
xgen.setNextIsAttribute(true);
}
final int textIndex = _textPropertyIndex;
final QName[] xmlNames = _xmlNames;
final BitSet cdata = _cdata;
int i = 0;
try {
for (final int len = props.length; i < len; ++i) {
// 28-jan-2014, pascal: we don't want to reset the attribute flag if we are an unwrapping serializer
// that started with nextIsAttribute to true because all properties should be unwrapped as attributes too.
if (i == attrCount && !(isAttribute && isUnwrappingSerializer())) {
xgen.setNextIsAttribute(false);
}
// also: if this is property to write as text ("unwrap"), need to:
if (i == textIndex) {
xgen.setNextIsUnwrapped(true);
}
xgen.setNextName(xmlNames[i]);
BeanPropertyWriter prop = props[i];
if (prop != null) { // can have nulls in filtered list
if ((cdata != null) && cdata.get(i)) {
xgen.setNextIsCData(true);
filter.serializeAsProperty(bean, xgen, ctxt, prop);
xgen.setNextIsCData(false);
} else {
filter.serializeAsProperty(bean, xgen, ctxt, prop);
}
}
// Reset to avoid next value being written as unwrapped,
// for example when property is suppressed
if (i == textIndex) {
xgen.setNextIsUnwrapped(false);
}
}
} catch (Exception e) {
String name = (i == props.length) ? "[anySetter]" : props[i].getName();
wrapAndThrow(ctxt, e, bean, name);
} catch (StackOverflowError e) {
final String name = (i == props.length) ? "[anySetter]" : props[i].getName();
throw DatabindException.from(gen0, "Infinite recursion (StackOverflowError)", e)
.prependPath(bean, name);
}
}
@Override
public void serializeWithType(Object bean, JsonGenerator gen, SerializationContext ctxt,
TypeSerializer typeSer)
throws JacksonException
{
if (_objectIdWriter != null) {
// [dataformat-xml#81]: XML cannot represent typed object id references
// as arrays ([type, id]) inline within lists, unlike JSON. So for
// back-references, write just the id without type wrapping.
WritableObjectId oid = ctxt.findObjectId(bean, _objectIdWriter.generator);
if (oid.writeAsReference(gen, ctxt, _objectIdWriter)) {
return;
}
_serializeWithObjectId(bean, gen, ctxt, typeSer);
return;
}
// Ok: let's serialize type id as attribute, but if (and only if!)
// we are using AS_PROPERTY -- and only when writing real XML. During
// convertValue()/valueToTree() `gen` is a TokenBuffer/tree generator,
// so skip the attribute handling (as `_serializeProperties` does).
if (typeSer.getTypeInclusion() == JsonTypeInfo.As.PROPERTY
&& gen instanceof ToXmlGenerator xgen) {
xgen.setNextIsAttribute(true);
super.serializeWithType(bean, gen, ctxt, typeSer);
if (_attributeCount == 0) { // if no attributes, need to reset
xgen.setNextIsAttribute(false);
}
} else {
super.serializeWithType(bean, gen, ctxt, typeSer);
}
}
@Override
protected void _serializeObjectId(Object bean, JsonGenerator gen, SerializationContext ctxt,
TypeSerializer typeSer, WritableObjectId objectId)
throws JacksonException
{
// Ok: let's serialize type id as attribute, but if (and only if!) we are
// using AS_PROPERTY -- and only when writing real XML (not a TokenBuffer
// from convertValue()/valueToTree()).
if (typeSer.getTypeInclusion() == JsonTypeInfo.As.PROPERTY
&& gen instanceof ToXmlGenerator xgen) {
xgen.setNextIsAttribute(true);
super._serializeObjectId(bean, gen, ctxt, typeSer, objectId);
if (_attributeCount == 0) { // if no attributes, need to reset
xgen.setNextIsAttribute(false);
}
} else {
super._serializeObjectId(bean, gen, ctxt, typeSer, objectId);
}
}
/*
/**********************************************************************
/* Helper methods
/**********************************************************************
*/
protected static boolean _isAttribute(BeanPropertyWriter bpw)
{
XmlInfo info = (XmlInfo) bpw.getInternalSetting(KEY_XML_INFO);
return (info != null) && info.isAttribute();
}
protected static boolean _isCData(BeanPropertyWriter bpw)
{
XmlInfo info = (XmlInfo) bpw.getInternalSetting(KEY_XML_INFO);
return (info != null) && info.isCData();
}
/**
* Method for re-sorting lists of bean properties such that attributes are strictly
* written before elements.
*/
protected static int _orderAttributesFirst(BeanPropertyWriter[] properties,
BeanPropertyWriter[] filteredProperties)
{
int attrCount = 0;
for (int i = 0, len = properties.length; i < len; ++i) {
BeanPropertyWriter bpw = properties[i];
if (!_isAttribute(bpw)) {
continue;
}
// Move attribute a few places down as necessary
int moveBy = i - attrCount;
if (moveBy > 0) {
System.arraycopy(properties, attrCount, properties, attrCount + 1, moveBy);
properties[attrCount] = bpw;
if (filteredProperties != null) {
BeanPropertyWriter fbpw = filteredProperties[i];
System.arraycopy(filteredProperties, attrCount, filteredProperties, attrCount+1, moveBy);
filteredProperties[attrCount] = fbpw;
}
}
++attrCount;
}
return attrCount;
}
/*
/**********************************************************************
/* Helper classes
/**********************************************************************
*/
/**
* Holder for the four index-parallel structures that describe XML output of
* a set of properties: number of leading attributes, index of the text
* ("unwrapped") property, per-index element/attribute {@link QName}s, and the
* set of indexes to write as CDATA.
*
* @since 3.3
*/
private static final class XmlInfoInternal {
final int attributeCount;
final int textPropertyIndex;
final QName[] xmlNames;
final BitSet cdata;
XmlInfoInternal(int attributeCount, int textPropertyIndex,
QName[] xmlNames, BitSet cdata) {
this.attributeCount = attributeCount;
this.textPropertyIndex = textPropertyIndex;
this.xmlNames = xmlNames;
this.cdata = cdata;
}
}
}