XmlDeserializationContext.java

package tools.jackson.dataformat.xml.deser;

import javax.xml.namespace.QName;

import tools.jackson.core.FormatSchema;
import tools.jackson.core.JacksonException;
import tools.jackson.core.TokenStreamFactory;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.DeserializationContextExt;
import tools.jackson.databind.deser.DeserializerCache;
import tools.jackson.databind.deser.DeserializerFactory;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.databind.util.TokenBuffer;
import tools.jackson.dataformat.xml.XmlFactory;
import tools.jackson.dataformat.xml.XmlReadFeature;
import tools.jackson.dataformat.xml.util.XmlRootNameLookup;

/**
 * XML-specific {@link DeserializationContext} needed to override certain
 * handlers.
 */
public class XmlDeserializationContext
    extends DeserializationContextExt
{
    private final String _xmlTextElementName;

    /**
     * @since 3.2
     */
    protected final XmlRootNameLookup _rootNameLookup;

    public XmlDeserializationContext(TokenStreamFactory tsf,
            DeserializerFactory deserializerFactory, DeserializerCache cache,
            DeserializationConfig config, FormatSchema schema,
            InjectableValues values,
            XmlRootNameLookup rootNameLookup) {
        super(tsf, deserializerFactory, cache,
                config, schema, values);
        _xmlTextElementName = ((XmlFactory) tsf).getXMLTextElementName();
        _rootNameLookup = rootNameLookup;
    }

    /*
    /**********************************************************************
    /* Overrides we need
    /**********************************************************************
     */

    @Override
    public Object readRootValue(JsonParser p, JavaType valueType,
            ValueDeserializer<Object> deser, Object valueToUpdate)
        throws JacksonException
    {
        // [dataformat-xml#247]: Verify root element name if feature enabled
        if (p instanceof FromXmlParser xp
                && xp.isEnabled(XmlReadFeature.ENFORCE_ROOT_ELEMENT_NAME)) {
            _verifyRootElementName(xp, valueType);
        }

        // 18-Sep-2021, tatu: Complicated mess; with 2.12, had [dataformat-xml#374]
        //    to disable handling. With 2.13, via [dataformat-xml#485] undid this change
        if (_config.useRootWrapping()) {
            return _unwrapAndDeserialize(p, valueType, deser, valueToUpdate);
        }
        if (valueToUpdate == null) {
            return deser.deserialize(p, this);
        }
        return deser.deserialize(p, this, valueToUpdate);
    }

    // To support case where XML element has attributes as well as CDATA, need
    // to "extract" scalar value (CDATA), after the fact
    @Override
    public String extractScalarFromObject(JsonParser p, ValueDeserializer<?> deser,
            Class<?> scalarType)
        throws JacksonException
    {
        // Only called on START_OBJECT, should not need to check, but JsonParser we
        // get may or may not be `FromXmlParser` so traverse using regular means
        String text = "";

        while (p.nextToken() == JsonToken.PROPERTY_NAME) {
            // Couple of ways to find "real" textual content. One is to look for
            // "XmlText"... but for that would need to know configuration. Alternatively
            // could hold on to last text seen -- but this might be last attribute, for
            // empty element. So for now let's simply hard-code check for expected
            // "text element" marker/placeholder and hope for best
            final String propName = p.currentName();
            JsonToken t = p.nextToken();
            if (t == JsonToken.VALUE_STRING) {
                if (propName.equals(_xmlTextElementName)) {
                    text = p.getString();
                }
            } else {
                p.skipChildren();
            }
        }
        return text;
    }

    /**
     * Override to return XML-aware {@link XmlTokenBuffer} that produces
     * parsers implementing {@link ElementWrappable}, allowing virtual wrapping
     * to be configured even after content has been buffered (e.g., during
     * polymorphic type resolution).
     *
     * @since 3.2
     */
    @Override
    public TokenBuffer bufferForInputBuffering(JsonParser p) {
        return XmlTokenBuffer.xmlBufferForInputBuffering(p, this);
    }

    /*
    /**********************************************************************
    /* Internal helper methods
    /**********************************************************************
     */

    /**
     * Helper method for [dataformat-xml#247]: verify that the root element name
     * matches the expected name when {@link XmlReadFeature#ENFORCE_ROOT_ELEMENT_NAME}
     * is enabled.
     *
     * @since 3.2
     */
    protected void _verifyRootElementName(FromXmlParser xp, JavaType valueType)
        throws JacksonException
    {
        QName rootName = xp.getRootElementName();
        if (rootName == null) {
            return;
        }
        String actualName = rootName.getLocalPart();
        QName expectedQName = _rootNameLookup.findRootName(this, valueType);
        String expectedName = expectedQName.getLocalPart();

        if (!expectedName.equals(actualName)) {
            reportPropertyInputMismatch(valueType, actualName,
                    "Root name \"%s\" does not match expected (\"%s\") for type %s",
                    actualName, expectedName, ClassUtil.getTypeDescription(valueType));
        }

        // Also verify namespace URI: must match both ways (unexpected namespace
        // present, or expected namespace missing)
        String expectedNs = expectedQName.getNamespaceURI();
        String actualNs = rootName.getNamespaceURI();
        boolean expectedEmpty = (expectedNs == null || expectedNs.isEmpty());
        boolean actualEmpty = (actualNs == null || actualNs.isEmpty());

        if (expectedEmpty != actualEmpty || (!expectedEmpty && !expectedNs.equals(actualNs))) {
            reportPropertyInputMismatch(valueType, actualName,
                    "Root namespace \"%s\" does not match expected (\"%s\") for type %s",
                    _nsDesc(actualNs), _nsDesc(expectedNs),
                    ClassUtil.getTypeDescription(valueType));
        }
    }

    private static String _nsDesc(String ns) {
        return (ns == null || ns.isEmpty()) ? "" : ns;
    }
}