XmlBeanDeserializerModifier.java

package tools.jackson.dataformat.xml.deser;

import java.util.*;

import tools.jackson.databind.*;
import tools.jackson.databind.deser.*;
import tools.jackson.databind.deser.bean.BeanDeserializerBase;
import tools.jackson.databind.deser.std.DelegatingDeserializer;
import tools.jackson.databind.introspect.AnnotatedMember;
import tools.jackson.databind.introspect.BeanPropertyDefinition;
import tools.jackson.dataformat.xml.util.AnnotationUtil;

/**
 * The main reason for a modifier is to support handling of
 * 'wrapped' Collection types.
 */
public class XmlBeanDeserializerModifier
    extends ValueDeserializerModifier
    implements java.io.Serializable
{
    private static final long serialVersionUID = 1L;

    /**
     * Virtual name used for text segments.
     */
    protected final String _cfgNameForTextValue;

    public XmlBeanDeserializerModifier(String nameForTextValue)
    {
        _cfgNameForTextValue = nameForTextValue;
    }

    @Override
    public List<BeanPropertyDefinition> updateProperties(DeserializationConfig config,
            BeanDescription.Supplier beanDescRef, List<BeanPropertyDefinition> propDefs)
    {
        final AnnotationIntrospector intr = config.getAnnotationIntrospector();
        int changed = 0;
        
        for (int i = 0, propCount = propDefs.size(); i < propCount; ++i) {
            BeanPropertyDefinition prop = propDefs.get(i);
            AnnotatedMember acc = prop.getPrimaryMember();
            // should not be null, but just in case:
            if (acc == null) {
                continue;
            }
            // First: handle "as text"? Such properties are exposed as values of 'unnamed'
            // properties; so one way to map them is to rename property to have special
            // name (and hope this does not break other parts...)
            Boolean b = AnnotationUtil.findIsTextAnnotation(config, intr, acc);
            if (b != null && b.booleanValue()) {
                BeanPropertyDefinition newProp = prop.withSimpleName(_cfgNameForTextValue);
                if (newProp != prop) {
                    // 24-Mar-2026, tatu: Create defensive copy
                    if (changed == 0) {
                        propDefs = new ArrayList<>(propDefs);
                    }
                    ++changed;
                    propDefs.set(i, newProp);
                }
                continue;
            }
            // second: do we need to handle wrapping (for Lists)?
            PropertyName wrapperName = prop.getWrapperName();
            
            if (wrapperName != null && wrapperName != PropertyName.NO_NAME) {
                String localName = wrapperName.getSimpleName();
                if ((localName != null && localName.length() > 0)
                        && !localName.equals(prop.getName())) {
                    // make copy-on-write as necessary
                    if (changed == 0) {
                        propDefs = new ArrayList<>(propDefs);
                    }
                    ++changed;
                    propDefs.set(i, prop.withSimpleName(localName));
                    continue;
                }
                // otherwise unwrapped; needs handling but later on
            }
        }
        return propDefs;
    }

    @Override
    public ValueDeserializer<?> modifyDeserializer(DeserializationConfig config,
            BeanDescription.Supplier beanDescRef, ValueDeserializer<?> deser)
    {
        if (deser instanceof BeanDeserializerBase bdb) {
            return _modifyBeanDeserializer(config, bdb);
        }
        // [dataformat-xml#334]: If a user's DeserializerModifier has wrapped the
        //   BeanDeserializer in a DelegatingDeserializer, unwrap to find the
        //   underlying BeanDeserializerBase, process it, and rebuild the chain.
        if (deser instanceof DelegatingDeserializer dd) {
            return _modifyThroughDelegation(config, dd);
        }
        return deser;
    }

    // @since 3.2.0
    protected ValueDeserializer<?> _modifyBeanDeserializer(DeserializationConfig config,
            BeanDeserializerBase deser)
    {
        /* 17-Aug-2013, tatu: One important special case first: if we have one "XML Text"
         * property, it may be exposed as VALUE_STRING token (depending on whether any attribute
         * values are exposed): and to deserialize from that, we need special handling unless POJO
         * has appropriate single-string creator method.
         */
        // Heuristics are bit tricky; but for now let's assume that if POJO
        // can already work with VALUE_STRING, it's ok and doesn't need extra support
        ValueInstantiator inst = deser.getValueInstantiator();
        // 03-Aug-2017, tatu: [dataformat-xml#254] suggests we also should
        //    allow passing `int`/`Integer`/`long`/`Long` cases, BUT
        //    unfortunately we can not simply use default handling. Would need
        //    coercion.
        // 30-Apr-2020, tatu: Complication from [dataformat-xml#318] as we now
        //    have a delegate too...
        // [dataformat-xml#608]: relaxed from earlier check that required all
        // non-text properties to be attributes; now handles beans with
        // @JacksonXmlText alongside other element properties too.
        if (!inst.canCreateFromString()) {
            SettableBeanProperty textProp = _findTextProp(deser.properties());
            if (textProp != null) {
                return new XmlTextDeserializer(deser, textProp);
            }
        }
        return new WrapperHandlingDeserializer(deser);
    }

    // @since 3.2.0
    protected ValueDeserializer<?> _modifyThroughDelegation(DeserializationConfig config,
            DelegatingDeserializer deser)
    {
        ValueDeserializer<?> delegatee = deser.getDelegatee();
        ValueDeserializer<?> modifiedDelegatee;
        if (delegatee instanceof BeanDeserializerBase bdb) {
            modifiedDelegatee = _modifyBeanDeserializer(config, bdb);
        } else if (delegatee instanceof DelegatingDeserializer dd) {
            modifiedDelegatee = _modifyThroughDelegation(config, dd);
        } else {
            // Delegatee is not a type we can handle
            return deser;
        }
        if (modifiedDelegatee != delegatee) {
            return deser.replaceDelegatee(modifiedDelegatee);
        }
        return deser;
    }

    /**
     * Find the {@code @JacksonXmlText} property (renamed to {@code _cfgNameForTextValue}
     * by {@link #updateProperties}) if one exists.
     *<p>
     * NOTE: before [dataformat-xml#608] fix, this method also required all non-text
     * properties to be attributes; relaxed to allow element properties too.
     */
    private SettableBeanProperty _findTextProp(Iterator<SettableBeanProperty> propIt)
    {
        while (propIt.hasNext()) {
            SettableBeanProperty prop = propIt.next();
            PropertyName n = prop.getFullName();
            if (_cfgNameForTextValue.equals(n.getSimpleName())) {
                return prop;
            }
        }
        return null;
    }
}