/*
 * 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.sdo.helper;

import commonj.sdo.ChangeSummary;
import commonj.sdo.DataObject;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import javax.xml.namespace.QName;
import org.eclipse.persistence.sdo.SDOChangeSummary;
import org.eclipse.persistence.sdo.SDOConstants;
import org.eclipse.persistence.sdo.SDODataObject;
import org.eclipse.persistence.sdo.SDOProperty;
import org.eclipse.persistence.sdo.SDOSetting;
import org.eclipse.persistence.sdo.SDOType;
import org.eclipse.persistence.sessions.Session;
import org.eclipse.persistence.internal.oxm.Namespace;
import org.eclipse.persistence.internal.oxm.XMLConversionManager;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.oxm.NamespaceResolver;
import org.eclipse.persistence.oxm.XMLConstants;
import org.eclipse.persistence.oxm.XMLField;
import org.eclipse.persistence.oxm.XMLMarshalListener;
import org.eclipse.persistence.oxm.XMLMarshaller;
import org.eclipse.persistence.oxm.XMLRoot;
import org.eclipse.persistence.oxm.record.DOMRecord;
import org.eclipse.persistence.oxm.record.MarshalRecord;
import org.eclipse.persistence.platform.xml.XMLPlatformFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * <p><b>Purpose</b>: Implementation of XMLMarshalListener used when marshalling DataObjects to XML
 * <p><b>Responsibilities</b>:<ul>
 * <li> Need to do extra work to marshal org.eclipse.persistence.sdo.SDOChangeSummary objects
 * </ul>
 */
public class SDOMarshalListener implements XMLMarshalListener {
    // marshalledObject may or may not be the root object
    private Object marshalledObject;
    private QName marshalledObjectRootQName;
    private MarshalRecord rootMarshalRecord;
    private SDOTypeHelper typeHelper;

    /** maintain narrowed context from the larger HelperContext (inside the xmlMarshaller)<br>
     * Visibility reduced from [public] in 2.1.0. May 15 2007 */
    private XMLMarshaller xmlMarshaller;

    public SDOMarshalListener(XMLMarshaller aMarshaller, SDOTypeHelper aTypeHelper) {
        xmlMarshaller = aMarshaller;
        typeHelper = aTypeHelper;
    }

    public void afterMarshal(Object obj) {
    }

    public void beforeMarshal(Object obj) {
        if (obj instanceof SDOChangeSummary) {
            SDOChangeSummary changeSummary = ((SDOChangeSummary)obj);

            //CREATED - build a list of xpaths to write to the created attribute
            //this must be done dynamically because the xpath is relative to the marshalledObject
            //so it can't be calculated until we know what object is being marshalled
            List createdSet = changeSummary.getCreated();
            List xpaths = new ArrayList(createdSet.size());

            String rootElementName = this.marshalledObjectRootQName.getLocalPart();
            String rootNamespaceUri = this.marshalledObjectRootQName.getNamespaceURI();
            if(rootNamespaceUri != null && !rootNamespaceUri.equals(XMLConstants.EMPTY_STRING)) {
                org.eclipse.persistence.internal.oxm.NamespaceResolver resolver = getRootMarshalRecord().getNamespaceResolver();
                if(resolver != null) {
                    String prefix = resolver.resolveNamespaceURI(this.marshalledObjectRootQName.getNamespaceURI());
                    if(prefix != null) {
                        rootElementName = prefix + XMLConstants.COLON + rootElementName;
                    }
                }
            }
            if ((createdSet != null) && (createdSet.size() > 0)) {
                Iterator anIterator = createdSet.iterator();
                SDODataObject nextCreatedDO = null;
                while (anIterator.hasNext()) {
                    // get path to the changeSummaryRoot (may not be the root marshalled object - may be internal)
                    nextCreatedDO = ((SDODataObject)anIterator.next());
                    String nextPath = getPathFromAncestor(nextCreatedDO, (SDODataObject)marshalledObject, changeSummary);
                    //Add sdoRef attribute...all modified objects written should have this
                    if(nextPath == SDOConstants.EMPTY_STRING) {
                        //if this is the root, just put the root element
                        xpaths.add(SDOConstants.SDO_CHANGESUMMARY_REF_PATH_PREFIX +
                                SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT +//
                                rootElementName);

                    } else {
                        xpaths.add(SDOConstants.SDO_CHANGESUMMARY_REF_PATH_PREFIX +
                                SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT +//
                                rootElementName + SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT + nextPath);
                    }
                }
            }
            changeSummary.setCreatedXPaths(xpaths);

            //Build xpathToCS
            String xpathMarshalledObjToCS = getPathFromAncestor(changeSummary.getRootObject(), (SDODataObject)marshalledObject, changeSummary);
            String xpathChangeSumProp = getXPathForProperty(changeSummary.getRootObject().getType().getChangeSummaryProperty());

            String xpathToCS = SDOConstants.SDO_CHANGESUMMARY_REF_PATH_PREFIX + SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT + rootElementName;

            // check if the CS is at the local-cs-root or is in a child property
            if ((xpathMarshalledObjToCS != null) && !xpathMarshalledObjToCS.equals(SDOConstants.EMPTY_STRING)) {//SDO_XPATH_TO_ROOT)) {
                // CS is not on the root
                xpathToCS = xpathToCS + SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT + xpathMarshalledObjToCS;
            }
            xpathToCS = xpathToCS + SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT//
                         +xpathChangeSumProp + SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT;

            //MODIFIED && DELETED
            List deletedXPaths = new ArrayList();
            Document document = XMLPlatformFactory.getInstance().getXMLPlatform().createDocument();
            Element csNode = null;
            List modifiedItems = changeSummary.getModified();
            int modifiedSize = modifiedItems.size();
            List newNodes = new ArrayList(modifiedSize);
            SDODataObject nextModifiedDO = null;

            //Iterate through CS modified items
            for (int i = 0; i < modifiedSize; i++) {
                nextModifiedDO = (SDODataObject)modifiedItems.get(i);
                String sdoPrefix = typeHelper.getPrefix(SDOConstants.SDO_URL);

                //List unsetPropNames = new ArrayList();
                String uri = getURI(nextModifiedDO);
                String qualifiedName = getQualifiedName(nextModifiedDO);
                String sdoRefPrefix = SDOConstants.SDO_CHANGESUMMARY_REF_PATH_PREFIX + SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT;

                if (uri == null) {
                    csNode = document.createElement(qualifiedName);
                } else {
                    csNode = document.createElementNS(uri, qualifiedName);
                }

                String nextPath = getPathFromAncestor(nextModifiedDO, (SDODataObject)marshalledObject, changeSummary);
                //Add sdoRef attribute...all modified objects written should have this
                if(nextPath == SDOConstants.EMPTY_STRING) {
                    //if this is the root, just put the root element
                    csNode.setAttributeNS(SDOConstants.SDO_URL, sdoPrefix +//
                            SDOConstants.SDO_XPATH_NS_SEPARATOR_FRAGMENT +//
                            SDOConstants.CHANGESUMMARY_REF,//
                            sdoRefPrefix + rootElementName);

                } else {
                    csNode.setAttributeNS(SDOConstants.SDO_URL, sdoPrefix +//
                                      SDOConstants.SDO_XPATH_NS_SEPARATOR_FRAGMENT +//
                                      SDOConstants.CHANGESUMMARY_REF,//
                                      sdoRefPrefix + rootElementName + "/" + nextPath);
                }

                //Bug6346754 Add all namespaces if they are not yet declared above.
                Vector namespaces = nextModifiedDO.getType().getXmlDescriptor().getNonNullNamespaceResolver().getNamespaces();
                for (int j = 0; j < namespaces.size(); j++) {
                    Namespace next = (Namespace)namespaces.get(j);
                    if (declareNamespace(next.getNamespaceURI(), next.getPrefix(), changeSummary.getRootObject())) {
                        csNode.setAttributeNS(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI, javax.xml.XMLConstants.XMLNS_ATTRIBUTE + XMLConstants.COLON + next.getPrefix(), next.getNamespaceURI());
                    }
                }

                List nextDOSettings = changeSummary.getOldValues(nextModifiedDO);
                DOMRecord row = new DOMRecord(csNode);

                Session session = ((SDOXMLHelper)typeHelper.getHelperContext().getXMLHelper()).getXmlContext().getSession();
                row.setSession((AbstractSession) session);

                //Iterate through SDOSettings for the current modified Object
                SDOSetting nextSetting = null;
                for (int j = 0; j < nextDOSettings.size(); j++) {
                    nextSetting = (SDOSetting)nextDOSettings.get(j);
                    if (nextSetting.isSet()) {
                        if (!nextSetting.getProperty().getType().isDataType()) {
                            if (nextSetting.getProperty().isMany()) {
                                List values = (List)nextSetting.getValue();
                                for (int k = 0; k < values.size(); k++) {
                                    doMarshal(nextSetting.getProperty(), (DataObject)values.get(k),//
                                              changeSummary, csNode, nextModifiedDO, deletedXPaths, xpathToCS, sdoPrefix, rootElementName);
                                }
                            } else {
                                doMarshal(nextSetting.getProperty(), (DataObject)nextSetting.getValue(),//
                                          changeSummary, csNode, nextModifiedDO, deletedXPaths, xpathToCS, sdoPrefix, rootElementName);
                            }
                        } else {
                            //This writes out simple values
                            Object value = nextSetting.getValue();

                            if (value == null) {
                                //Marshal out xsi:nil=true
                                marshalNilAttribute(nextSetting.getProperty(), row);
                            } else {
                                String xPath = getXPathForProperty(nextSetting.getProperty());
                                XMLField field = new XMLField(xPath);
                                field.setNamespaceResolver(typeHelper.getNamespaceResolver());
                                row.put(field, value);
                            }
                        }
                    }
                }

                List unsetPropNames = changeSummary.getUnsetProps(nextModifiedDO);
                if (!unsetPropNames.isEmpty()) {
                    XMLConversionManager xmlConversionManager = ((SDOXMLHelper)typeHelper.getHelperContext().getXMLHelper()).getXmlConversionManager();
                    String unsetPropsString = (String)xmlConversionManager.convertObject(unsetPropNames, String.class);
                    csNode.setAttributeNS(SDOConstants.SDO_URL, sdoPrefix +//
                                          SDOConstants.SDO_XPATH_NS_SEPARATOR_FRAGMENT +//
                                          SDOConstants.CHANGESUMMARY_UNSET, unsetPropsString);
                }
                newNodes.add(csNode);
            }

            changeSummary.setDeletedXPaths(deletedXPaths);
            changeSummary.setModifiedDoms(newNodes);
        }
    }

    private void doMarshal(SDOProperty prop, DataObject value, SDOChangeSummary cs,//
                           Element csNode, SDODataObject modifiedObject, List deletedXPaths, String xpathToCS, String sdoPrefix, String rootElementName) {
        if (value == null) {
            //Marshal out xsi:nil=true
            DOMRecord row = new DOMRecord(csNode);
            Session session = ((SDOXMLHelper)typeHelper.getHelperContext().getXMLHelper()).getXmlContext().getSession();
            row.setSession((AbstractSession) session);
            marshalNilAttribute(prop, row);
            return;
        }

        boolean isDeleted = false;

        //Do we need a second map or can we just check the values of the deleted map
        Object original = cs.getReverseDeletedMap().get(value);

        if ((original != null) && cs.isDeleted((DataObject)original)) {
            isDeleted = true;
        }

        String qualifiedName = getXPathForProperty(prop);
        String uri = null;
        if (prop.isOpenContent()) {
            uri = prop.getUri();
        } else {
            uri = ((XMLField)prop.getXmlMapping().getField()).getXPathFragment().getNamespaceURI();
        }

        if (isDeleted) {
            String pathToNode = getPathFromAncestor(((SDODataObject)original), modifiedObject, cs);
            String containerPath = null;
            containerPath = getQualifiedName(modifiedObject);
            deletedXPaths.add(xpathToCS + containerPath + SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT + pathToNode);

            XMLRoot xmlroot = new XMLRoot();
            xmlroot.setObject(value);//set the object to the deep copy
            xmlroot.setNamespaceURI(uri);
            xmlroot.setLocalName(qualifiedName);
            xmlMarshaller.marshal(xmlroot, csNode);
        } else {
            //need sdoref
            Element modifiedElement = null;
            if (uri == null) {
                modifiedElement = csNode.getOwnerDocument().createElement(qualifiedName);
            } else {
                modifiedElement = csNode.getOwnerDocument().createElementNS(uri, qualifiedName);
            }
            csNode.appendChild(modifiedElement);
            String nextPath = getPathFromAncestor((SDODataObject)original, (SDODataObject)marshalledObject, cs);
            //Add sdoRef attribute...all modified objects written should have this
            if(nextPath == SDOConstants.EMPTY_STRING) {
                //if this is the root, just put the root element
                modifiedElement.setAttributeNS(SDOConstants.SDO_URL, sdoPrefix +//
                        SDOConstants.SDO_XPATH_NS_SEPARATOR_FRAGMENT +//
                        SDOConstants.CHANGESUMMARY_REF,//
                        SDOConstants.SDO_CHANGESUMMARY_REF_PATH_PREFIX +
                        SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT +//
                        rootElementName);

            } else {
                modifiedElement.setAttributeNS(SDOConstants.SDO_URL, sdoPrefix +//
                                  SDOConstants.SDO_XPATH_NS_SEPARATOR_FRAGMENT +//
                                  SDOConstants.CHANGESUMMARY_REF,//
                                  SDOConstants.SDO_CHANGESUMMARY_REF_PATH_PREFIX +
                                  SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT +//
                                  rootElementName + "/" + nextPath);
            }

            //Added for bug 6346754
            if ((((SDODataObject)original).getContainmentProperty() != null) && ((SDODataObject)original).getContainmentProperty().getType().isDataObjectType()) {
                //may also need xsi:type
                String schemaContext = ((SDOType)value.getType()).getXmlDescriptor().getSchemaReference().getSchemaContext();
                QName schemaContextQName = ((SDOType)value.getType()).getXmlDescriptor().getSchemaReference().getSchemaContextAsQName(((SDOType)value.getType()).getXmlDescriptor().getNonNullNamespaceResolver());

                if (schemaContext != null) {
                    String typeValue = schemaContext.substring(1, schemaContext.length());
                    String schemaInstancePrefix = ((SDOType)value.getType()).getXmlDescriptor().getNonNullNamespaceResolver().resolveNamespaceURI(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);

                    //may or may not need the xmlns declaration added.
                    String schemaContextUri = schemaContextQName.getNamespaceURI();
                    String schemaContextPrefix = ((SDOType)value.getType()).getXmlDescriptor().getNonNullNamespaceResolver().resolveNamespaceURI(schemaContextUri);
                    if (schemaContextPrefix != null) {
                        modifiedElement.setAttributeNS(javax.xml.XMLConstants.XMLNS_ATTRIBUTE_NS_URI, javax.xml.XMLConstants.XMLNS_ATTRIBUTE + XMLConstants.COLON + schemaContextPrefix, schemaContextQName.getNamespaceURI());
                    }
                    modifiedElement.setAttributeNS(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, schemaInstancePrefix + XMLConstants.COLON + XMLConstants.SCHEMA_TYPE_ATTRIBUTE, typeValue);
                }
            }
        }
    }
    private String getURI(SDODataObject currentObject) {
        String uri = null;

        // check if we are at the root - no containmentProperty
        if (currentObject.getContainmentProperty() == null) {
            // get ns from type/schemaReference/schemaContext = "/ns0:PurchaseOrderType"
            // get ns:name from the ns=nsr(value==uri) name=, or 2nd entry in xmlDescriptor->table[]
            //containerPath = ((SDOType)modifiedObject.getType()).getXmlDescriptor().getNamespaceResolver().resolveNamespaceURI(uri);
            // get uri from the type of the root object
            uri = currentObject.getType().getURI();
        } else {
            uri = getUriForProperty(currentObject);
        }
        return uri;
    }

    private String getQualifiedName(SDODataObject currentObject) {
        String qualifiedName = null;

        // check if we are at the root - no containmentProperty
        if (currentObject.getContainmentProperty() == null) {
            // get ns (without name) from type/schemaReference/schemaContext = "/ns0:PurchaseOrderType"
            //qualifiedName = ((SDOType)currentObject.getType()).getXmlDescriptor().getNamespaceResolver().resolveNamespaceURI(uri);
            // property id's do not match - don't use
            //qualifiedName += ((SDOXSDHelper)currentObject.getHelperContext().getXSDHelper()).getGlobalProperty(uri, currentObject.getType().getName() , true);
            // hack: get 2nd table name
            // get ns:name from the ns=nsr(value==uri) name=, or 2nd entry in xmlDescriptor->table[]
            qualifiedName = currentObject.getType().getXmlDescriptor().getDefaultRootElement();
        } else {
            qualifiedName = getXPathForProperty(currentObject.getContainmentProperty());
        }
        return qualifiedName;
    }

    public void setMarshalledObject(Object marshalledObject) {
        this.marshalledObject = marshalledObject;
    }

    public Object getMarshalledObject() {
        return marshalledObject;
    }

    public void setMarshalledObjectRootQName(QName rootQName) {
        this.marshalledObjectRootQName = rootQName;
    }

    public QName getMarshalledObjectRootQName() {
        return this.marshalledObjectRootQName;
    }

    public void setRootMarshalRecord(MarshalRecord rootRecord) {
        this.rootMarshalRecord = rootRecord;
    }

    public MarshalRecord getRootMarshalRecord() {
        return this.rootMarshalRecord;
    }

    private boolean declareNamespace(String uri, String prefix, DataObject theDataObject) {
        while (theDataObject != null) {
            NamespaceResolver nr = ((SDOType)theDataObject.getType()).getXmlDescriptor().getNonNullNamespaceResolver();
            String resolvedPrefix = nr.resolveNamespaceURI(uri);
            if ((resolvedPrefix != null) && !resolvedPrefix.equals("") && resolvedPrefix.equals(prefix)) {
                return false;
            }
            theDataObject = theDataObject.getContainer();
        }
        return true;
    }

    private String getUriForProperty(SDODataObject currentObject) {
        SDOProperty prop = currentObject.getContainmentProperty();
        if (prop.getXmlMapping() != null) {
            return ((XMLField)prop.getXmlMapping().getField()).getXPathFragment().getNamespaceURI();
        } else {
            return prop.getUri();
        }
    }

    private String getXPathForProperty(SDOProperty prop) {
        return getXPathForProperty(prop, false, typeHelper.getNamespaceResolver());
    }

    private String getXPathForProperty(SDOProperty prop, boolean removeText, NamespaceResolver namespaceResolver) {
        if ((prop).getXmlMapping() != null) {
            String xpath = prop.getXmlMapping().getField().getName();
            if (removeText && xpath.endsWith("/text()")) {
                xpath = xpath.substring(0, xpath.length() - 7);
            }
            return xpath;
        } else {
            String name = prop.getName();
            if (prop.isOpenContent()) {
                String uri = prop.getUri();
                if (uri != null) {
                    String prefix = namespaceResolver.resolveNamespaceURI(uri);

                    if ((prefix != null) && !prefix.equals(SDOConstants.EMPTY_STRING)) {
                        return prefix + XMLConstants.COLON + name;
                    }
                }
            }
            return name;
        }
    }

    /**
    * INTERNAL:
    * Return the XPath or SDO path from the anObject to the current internal node
    *
    * Prereq: We know that the targetObject will always have a parent as called
    * from getPath()
    *   We require a ChangeSummary object for when there are deleted
    *   objects in the path
    *
    * Matching conditions:
    *   Iterate up the tree
    *   return a non-null string for the XPath when we reach the target node
    *
    * Function is partially based on SDOCopyHelper.copy(DataObject dataObject)
    * Performance: This function is O(log n) where n=# of children in the tree
    *
    * @param currentPath
    * @param targetObject
    * @param currentObject
    * @param aSeparator (XPath separator is written only between elements - not for the first call)
    * @return String (representing the XPath)
    */
    private String getPathFromAncestorPrivate(SDOChangeSummary aChangeSummary,//
                                              String currentPath,//
                                              SDODataObject targetDO,//
                                              SDODataObject currentObject,//
                                              String aSeparator) {
        if ((currentObject == null) || (targetDO == null) || (aChangeSummary == null)) {
            return currentPath;
        }

        // Base Case: check we are at the target object first
        if (currentObject == targetDO) {
            // check for indexed property if root is a ListWrapper
            return currentPath;
        }

        // Recursive Case: O(log(n)) recursive calls, 1 for each tree level
        // get parent property based on parent property name in target, property will always be set
        // check containment for cases where we are searching for a sibling
        SDOProperty parentContainmentProperty;
        Object parent = null;

        // for already deleted dataobjects  - isDeleted=false, changeSummary= null - use oldContainer
        if (null == currentObject.getContainer()) {
            parent = aChangeSummary.getOldContainer(currentObject);
            parentContainmentProperty = aChangeSummary.getOldContainmentProperty(currentObject);
            // handle (at root) case for non-deleted objects for the cases
            // case: ancestor not found
            // case: ancestor is actually sibling
            if ((null == parent) || (null == parentContainmentProperty)) {
                return SDOConstants.SDO_XPATH_INVALID_PATH;
            }
        } else {
            // normal non-deleted non-changeSummary case
            parent = currentObject.getContainer();
            parentContainmentProperty = currentObject.getContainmentProperty();
        }

        // get XPath using SDO path - block
        String parentContainmentPropertyXPath = getXPathForProperty(parentContainmentProperty);

        // Handle ListWrapper contained DataObjects
        if (parentContainmentProperty.isMany()) {
            int index = (((SDODataObject)parent).getList(parentContainmentProperty)).indexOf(currentObject);

            if (index < 0) {

                /*
                * The current object has been deleted and was part of a ListWrapper (isMany=true)
                * Get the parent of this indexed list item and check the oldSetting (List) for the
                * original position of the indexed (Deleted) object
                */

                // get the list containing the old value of the item
                ChangeSummary.Setting anOldSetting = aChangeSummary.getOldValue((DataObject)parent, parentContainmentProperty);
                if (anOldSetting != null) {
                    // get index directly from oldSettings based on current object - where parent was not deleted
                    List aDeletedParent = (List)anOldSetting.getValue();

                    // bug# 5587042: we will assume that index is never < 0 and remove handling code for this case where we lookup anOldSetting directly instead of via the deepCopies map
                    index = aDeletedParent.indexOf(aChangeSummary.getDeepCopies().get(currentObject));
                } else {
                    // bug# 5587042: we will assume that oldSetting is never null and remove handling code for this case where we would hardcode to list.size()
                }

                // see: testGetXPathFromAncestorDeletedFromChildToAncestorInsideListWrapperLoggingOn
            }
            currentPath = parentContainmentPropertyXPath +//
                          SDOConstants.SDO_XPATH_LIST_INDEX_OPEN_BRACKET +//
                          (1 + index) +// [indexes] start at 1
                          SDOConstants.SDO_XPATH_LIST_INDEX_CLOSE_BRACKET +//
                          aSeparator +//
                          currentPath;
        } else {
            currentPath = parentContainmentPropertyXPath +//
                          aSeparator +//
                          currentPath;
        }

        // recursive call
        return getPathFromAncestorPrivate(aChangeSummary,//
                                          currentPath,//
                                          targetDO,//
                                          (SDODataObject)parent,//
                                          SDOConstants.SDO_XPATH_SEPARATOR_FRAGMENT);// we pass an empty separator so we have \a\b and not a\b\
    }

    /**
     * INTERNAL:
     * Get path for non-deleted DataObjects<br>
     * ChangeSummary is not required and is set to null.<br>
     * Assumptions:<br>
     *     target node is an ancestor of the source (this)
     * @param sourceDO
     * @param targetDO
     * @return String xpath
     */
    public String getPathFromAncestor(SDODataObject sourceDO, SDODataObject targetDO, SDOChangeSummary cs) {
        // Implementors: SDOMarshalListener
        // default to no changeSummary and xpath format

        /*
        * Algorithm:
        *   (1) Intact (non-deleted) objects:
        *     - recursively iterate up the container of each DataObject, recording property names as we go
        *   (2) Deleted objects:
        *     - use the changeSummary to get the deleted object with oldContainer state
        *     - recursively iterate up the oldContainer as in (1)
        *    Issues:
        *     - a deleted indexed object inside a ListWrapper will not retain its original index
        */

        // Base Case: The internal node is actually the root
        // Base Case: The source and target objects are equal
        // checking if this and target are equal will handle both cases above
        if (sourceDO == targetDO) {
            // return "" empty string and handle at implementor
            return SDOConstants.EMPTY_STRING;
        } else {
            // Recursive Case: call private recursive reverse O(logn) traversal
            // function on current object
            return getPathFromAncestorPrivate(cs,//
                                              SDOConstants.EMPTY_STRING,//
                                              targetDO,//
                                              sourceDO,//
                                              SDOConstants.EMPTY_STRING// we pass an empty separator so we have \a\b and not a\b\
            );
        }
    }

    private void marshalNilAttribute(SDOProperty property, DOMRecord row) {
        //Marshal out xsi:nil=true
        NamespaceResolver resolver = null;
        if(property.getContainingType() != null) {
            resolver = property.getContainingType().getXmlDescriptor().getNamespaceResolver();
        }
        if(null == resolver) {
            resolver = typeHelper.getNamespaceResolver();
        }
        String xsiPrefix = resolver.resolveNamespaceURI(javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
        if ((xsiPrefix == null) || xsiPrefix.equals(SDOConstants.EMPTY_STRING)) {
            NamespaceResolver nsResolverWithXsi = new NamespaceResolver();
            copyNamespaces(resolver, nsResolverWithXsi);
            resolver = nsResolverWithXsi;
            xsiPrefix = resolver.generatePrefix(XMLConstants.SCHEMA_INSTANCE_PREFIX);
            resolver.put(xsiPrefix, javax.xml.XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
        }
        String xPath = getXPathForProperty(property, true, resolver);
        xPath = xPath + "/@" + xsiPrefix + XMLConstants.COLON + XMLConstants.SCHEMA_NIL_ATTRIBUTE;

        XMLField field = new XMLField(xPath);
        field.setNamespaceResolver(resolver);
        row.put(field, XMLConstants.BOOLEAN_STRING_TRUE);
    }

    private void copyNamespaces(NamespaceResolver source, NamespaceResolver target) {
        if (null != source && null != target) {
            if(source.hasPrefixesToNamespaces()) {
                target.getPrefixesToNamespaces().putAll(source.getPrefixesToNamespaces());
            }
            target.setDefaultNamespaceURI(source.getDefaultNamespaceURI());
        }
    }
}
