/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.oxm.mappings.nullpolicy;

import org.eclipse.persistence.core.sessions.CoreSession;
import org.eclipse.persistence.internal.core.sessions.CoreAbstractSession;
import org.eclipse.persistence.internal.oxm.Constants;
import org.eclipse.persistence.internal.oxm.NamespaceResolver;
import org.eclipse.persistence.internal.oxm.NullCapableValue;
import org.eclipse.persistence.internal.oxm.XPathEngine;
import org.eclipse.persistence.internal.oxm.XPathFragment;
import org.eclipse.persistence.internal.oxm.XPathNode;
import org.eclipse.persistence.internal.oxm.mappings.Field;
import org.eclipse.persistence.internal.oxm.record.AbstractMarshalRecord;
import org.eclipse.persistence.internal.oxm.record.MarshalRecord;
import org.eclipse.persistence.internal.oxm.record.XMLRecord;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;

/**
 * PUBLIC: <b>Description</b>: This node null policy allows for the handling of
 * various representations of null in XML documents.<br>
 * <p>
 * <b>Null policies have 2 concrete implementations</b>:
 * <ul>
 * <li> NullPolicy (default implementation)</li>
 * <li> IsSetNullPolicy (keyed off isSet() state of the node)</li>
 * </ul>
 *
 * <table summary="" border="1">
 * <tr>
 * <th id="c1" align="left">Unmarshal Flag</th>
 * <th id="c2" align="left">Description</th>
 * </tr>
 * <tr>
 * <td headers="c1"> isSetPerformedForAbsentNode </td>
 * <td headers="c2">This umarshal flag represents whether a set is done for
 * absent nodes only.</td>
 * </tr>
 * <tr>
 * <td headers="c1"> isNullRepresentedByEmptyNode </td>
 * <td headers="c2">If this unmarshal flag is false for empty nodes we set an
 * empty Object for composite mappings, otherwise we set to null.</td>
 * </tr>
 * <tr>
 * <td headers="c1" style="nowrap"> isNullRepresentedByXsiNil </td>
 * <td headers="c2">If this unmarshal flag is false for xsi:nil nodes we ignore
 * the xsi:nil attribute and treat as an empty node.<br>
 * Otherwise we set to null.</td>
 * </tr>
 * </table>
 *
 * <table summary="" border="1">
 * <tr>
 * <th id="c3" align="left">Marshal Enum</th>
 * <th id="c4" align="left">XMLNullRepresentationType Description</th>
 * </tr>
 * <tr>
 * <td headers="c3"> XSI_NIL </td>
 * <td headers="c4">Nillable: Write out an xsi:nil="true" attribute.</td>
 * </tr>
 * <tr>
 * <td headers="c3"> ABSENT_NODE(default) </td>
 * <td headers="c4">Optional: Write out no node.</td>
 * </tr>
 * <tr>
 * <td headers="c3" style="nowrap"> EMPTY_NODE </td>
 * <td headers="c4">Required: Write out an empty {@literal <node/>} or node="" node.</td>
 * </tr>
 * </table> &nbsp;<b>Usage</b>:<br>
 *
 * @see org.eclipse.persistence.internal.oxm.NullCapableValue
 * @since Oracle TopLink 11<i>g</i> Release 1 (11.1.1)
 */
public abstract class AbstractNullPolicy {
    protected static final String TRUE = "true";
    protected static final String COLON_W_SCHEMA_NIL_ATTRIBUTE = Constants.COLON + Constants.SCHEMA_NIL_ATTRIBUTE;
    protected static final String XSI_NIL_ATTRIBUTE = Constants.SCHEMA_INSTANCE_PREFIX + COLON_W_SCHEMA_NIL_ATTRIBUTE;

    /**
     * This state flag determines how we unmarshal absent nodes. true =
     * (default) Perform a set(null). false = Do not perform a set(null).
     */
    protected boolean isSetPerformedForAbsentNode = true;

    /**
     * This state flag determines how we unmarshal empty nodes. true = Perform a
     * set(null) or primitive type equivalent. false = (default) Perform a
     * set(new Object()).
     */
    protected boolean isNullRepresentedByEmptyNode = false;

    /**
     * This state flag determines how we unmarshal xsi:nil nodes. A set is
     * performed in both cases. true = Perform a set(null) or primitive type
     * equivalent.. false = (default) do nothing and treat as an empty node.
     */
    protected boolean isNullRepresentedByXsiNil = false;

    /**
     * This state flag determines how we unmarshal xsi:nil nodes when there
     * are other attributes (other than xsi:nil) present. If false, we ignore
     * any attributes and treat the element as nil. If true, we inspect if
     * some attributes are present and if yes, we process them.
     */
    protected boolean ignoreAttributesForNil = true;

    /**
     * This enum instance determines what to write out during a marshal
     * operation. We are defaulting to ABSENT_NODE
     */
    protected XMLNullRepresentationType marshalNullRepresentation = XMLNullRepresentationType.ABSENT_NODE;

    /**
     * Get the enum that determines what XML to write when a null value is encountered.
     */
    public XMLNullRepresentationType getMarshalNullRepresentation() {
        return marshalNullRepresentation;
    }

    /**
     * Set the enum that determines what XML to write when a null value is encountered.
     */
    public void setMarshalNullRepresentation(XMLNullRepresentationType anEnumInstance) {
        marshalNullRepresentation = anEnumInstance;
    }

    /**
     * INTERNAL:
     * When using the SAX or DOM Platform, this method is responsible for
     * marshalling null values for the XML Direct Mapping.
     *
     * @param xPathFragment
     * @param marshalRecord
     * @param object
     * @param session
     * @param namespaceResolver
     * @return true if this method caused any nodes to be marshaled, else false.
     */
    public boolean directMarshal(XPathFragment xPathFragment, MarshalRecord marshalRecord, //
                    Object object, CoreSession session, NamespaceResolver namespaceResolver) {
        // Handle attributes - XSI_NIL, ABSENT_NODE have the same behavior
        if (xPathFragment.isAttribute()) {
            // Write out an empty attribute
            if (marshalNullRepresentation == XMLNullRepresentationType.EMPTY_NODE) {
                marshalRecord.emptyAttribute(xPathFragment, namespaceResolver);
                return true;
            } else {
                // XSI_NIL attributes are invalid - and are ignored
                // ABSENT_NODE - Write out nothing
                return false;
            }
        } else {
            // Nillable: write out xsi:nil="true" attribute in empty element
            if (marshalNullRepresentation == XMLNullRepresentationType.XSI_NIL) {
                marshalRecord.nilSimple(namespaceResolver);
                return true;
            } else {
                // EMPTY_NODE - Write out empty element
                if (marshalNullRepresentation == XMLNullRepresentationType.EMPTY_NODE) {
                    marshalRecord.emptySimple(namespaceResolver);
                    return true;
                } else {
                    // ABSENT_NODE - Write out nothing
                    return false;
                }
            }
        }
    }

    /**
     * INTERNAL: When using the SAX Platform, this method is responsible for
     * marshalling null values for the XML Composite Object Mapping.
     *
     * @param xPathFragment
     * @param marshalRecord
     * @param object
     * @param session
     * @param namespaceResolver
     * @return true if this method caused any nodes to be marshaled, else false.
     */
    public boolean compositeObjectMarshal(XPathFragment xPathFragment, MarshalRecord marshalRecord, //
              Object object, CoreSession session, NamespaceResolver namespaceResolver) {
        if (marshalNullRepresentation == XMLNullRepresentationType.ABSENT_NODE){
            return false;
        }
        // Nillable
        else if (marshalNullRepresentation == XMLNullRepresentationType.XSI_NIL) {
            marshalRecord.nilComplex(xPathFragment, namespaceResolver);
            return true;
        } else if (marshalNullRepresentation == XMLNullRepresentationType.EMPTY_NODE) {
            // Optional and Required
            // This call is really only valid when using DOM - TBD false
            // Write out empty element - we need to differentiate between
            // object=null and object=new Object() with null fields and 0-numeric primitive values
            // EMPTY_NODE - Write out empty element - Required
                marshalRecord.emptyComplex(xPathFragment, namespaceResolver);
                return true;
        }
        return false;
    }

    /**
     * INTERNAL: When using the DOM Platform, this method is responsible for
     * marshalling null values for the XML Composite Object Mapping.
     *
     * @param record
     * @param object
     * @param field
     * @return true if this method caused any objects to be marshaled, else false.
     */
    public boolean compositeObjectMarshal(AbstractMarshalRecord record, Object object, Field field, CoreAbstractSession session) {
        if (marshalNullRepresentation == XMLNullRepresentationType.XSI_NIL) {
            record.put(field, XMLRecord.NIL);
            return true;
        } else {
            // EMPTY_NODE - Write out empty element - Required
            if (marshalNullRepresentation == XMLNullRepresentationType.EMPTY_NODE) {
                Node element = XPathEngine.getInstance().createUnownedElement(record.getDOM(), field);
                record.put(field, element);
                return true;
            } else {
                // ABSENT_NODE - Write out nothing - Optional
                return false;
            }
        }
    }

    /**
     * INTERNAL: When using the SAX or DOM Platform during unmarshal operations.
     * Use the attributes to determine if the element represents a null value.
     *
     * @param attributes
     * @return true if based on the attributes the corresponding element
     *         represents a null value, else false.
     */
    public boolean valueIsNull(Attributes attributes) {
        // Nillable
        if (isNullRepresentedByXsiNil()) {
            // Ignore any other attributes that are in addition to xsi:nil
            if(null == attributes) {
                return false;
            }
            return attributes.getValue(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, Constants.SCHEMA_NIL_ATTRIBUTE) != null;
        } else {
            // EMPTY_NODE - Required
            if (isNullRepresentedByEmptyNode() && (null == attributes || attributes.getLength() == 0)) {
                return true;
            }
        }
        return false;
    }


    /**
     * INTERNAL: When using the DOM Platform during unmarshal operations.
     * Use the element to determine if the element represents a null value.
     *
     * @param element
     * @return true if based on the element it represents a null value, else false.
     */
    public boolean valueIsNull(Element element) {
        // Check Nillable: Ignore any other attributes that are in addition to xsi:nil
        if (null == element) {
            return true;
        } else {
            if (isNullRepresentedByXsiNil() && element.hasAttributeNS(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, Constants.SCHEMA_NIL_ATTRIBUTE)) {
                return true;
            } else {
                // EMPTY_NODE - Required
                // Verify no attributes and no child nodes on the DOM element
                if (isNullRepresentedByEmptyNode() && !element.hasAttributes() && (element.getChildNodes().getLength() == 0)) {
                    return true;
                } else {
                    return false;
                }
            }
        }
    }

    /**
     * INTERNAL: When using the SAX Platform this allows a NodeValue to be
     * registered to receive events from the TreeObjectBuilder.
     * @param xPathNode
     * @param nullCapableValue
     */
    public abstract void xPathNode(XPathNode xPathNode, NullCapableValue nullCapableValue);

    /**
     * @return the isSetPerformedForAbsentNode flag
     */
    public boolean getIsSetPerformedForAbsentNode() {
        return isSetPerformedForAbsentNode;
    }

    /**
     *
     * @return
     */
    public boolean isNullRepresentedByEmptyNode() {
        return isNullRepresentedByEmptyNode;
    }

    /**
     *
     * @param bIsNullRepresentedByEmptyNode
     */
    public void setNullRepresentedByEmptyNode(boolean bIsNullRepresentedByEmptyNode) {
        isNullRepresentedByEmptyNode = bIsNullRepresentedByEmptyNode;
    }

    /**
     *
     * @return
     */
    public boolean isNullRepresentedByXsiNil() {
        return isNullRepresentedByXsiNil;
    }

    /**
     *
     * @param bIsNullRepresentedByXsiNil
     */
    public void setNullRepresentedByXsiNil(boolean bIsNullRepresentedByXsiNil) {
        isNullRepresentedByXsiNil = bIsNullRepresentedByXsiNil;
    }

    /**
     *
     * @return
     */
    public boolean ignoreAttributesForNil() {
        return ignoreAttributesForNil;
    }

    /**
     *
     * @param ignoreAttributesForNil
     */
    public void setIgnoreAttributesForNil(boolean ignoreAttributesForNil) {
        this.ignoreAttributesForNil = ignoreAttributesForNil;
    }

    /**
     * INTERNAL:
     * Private function to process or create an entry in the NamespaceResolver for the xsi prefix.
     * @param namespaceResolver
     * @return xsi prefix
     */
    protected String processNamespaceResolverForXSIPrefix(NamespaceResolver namespaceResolver, MarshalRecord marshalRecord) {
        String xsiPrefix;
        if (null == namespaceResolver) {
            // add new xsi entry into the properties map
            xsiPrefix = Constants.SCHEMA_INSTANCE_PREFIX;
              marshalRecord.namespaceDeclaration(xsiPrefix, javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
        } else {
            // find an existing xsi entry in the map
            xsiPrefix = namespaceResolver.resolveNamespaceURI(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
            if (null == xsiPrefix) {
                xsiPrefix = namespaceResolver.generatePrefix(Constants.SCHEMA_INSTANCE_PREFIX);
                marshalRecord.namespaceDeclaration(xsiPrefix, javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
            }
        }
        return xsiPrefix;
    }

    /**
     * INTERNAL
     */
    public void directMarshal(Field field, AbstractMarshalRecord record, Object object) {
        Object fieldValue = null;
        if(marshalNullRepresentation == XMLNullRepresentationType.EMPTY_NODE) {
            fieldValue = Constants.EMPTY_STRING;
        } else {
            if(!field.getLastXPathFragment().isAttribute()) {
                if(marshalNullRepresentation == XMLNullRepresentationType.XSI_NIL) {
                    fieldValue = XMLRecord.NIL;
                }
            }
        }
        record.put(field, fieldValue);
    }
}
