/*
 * 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:
//     bdoughan - Jan 27/2009 - 1.1 - Initial implementation
package org.eclipse.persistence.sdo.helper.jaxb;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.xml.bind.JAXBContext;
import javax.xml.namespace.QName;

import org.eclipse.persistence.exceptions.SDOException;
import org.eclipse.persistence.internal.helper.IdentityWeakHashMap;
import org.eclipse.persistence.internal.oxm.XPathFragment;
import org.eclipse.persistence.oxm.XMLDescriptor;
import org.eclipse.persistence.oxm.schema.XMLSchemaReference;
import org.eclipse.persistence.sdo.SDODataObject;
import org.eclipse.persistence.sdo.SDOType;
import org.eclipse.persistence.sdo.helper.SDOCopyHelper;
import org.eclipse.persistence.sdo.helper.SDODataHelper;
import org.eclipse.persistence.sdo.helper.SDOEqualityHelper;
import org.eclipse.persistence.sdo.helper.SDOHelperContext;
import org.eclipse.persistence.sdo.helper.delegates.SDOTypeHelperDelegate;
import org.eclipse.persistence.sdo.helper.delegates.SDOXSDHelperDelegate;

import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.Type;

/**
 * <p>The JAXBHelperContext is a bridge between POJOs and SDO DataObjects.  The bridge
 * is based on their corresponding XML representations.  For the POJOs the XML
 * representation is specified using JAXB annotations or object-to-XML mappings.</p>
 *
 * <p>The following steps are required to create the JAXBHelperContext.  The XML schema
 * used in step #3 is the same one that the POJOs are mapped to.  This step has been
 * separated so that SDO annotations could be added to the XML schema.
 * <p>Step #1 - Create the JAXBContext</p>
 * <pre>
 * JAXBContext jaxbContext = JAXBContext.newInstance("com.example.customer");
 * </pre>
 * <p>Step #2 - Create the JAXBHelperContext</p>
 * <pre>
 * JAXBHelperContext jaxbHelperContext = new JAXBHelperContext(jaxbContext);
 * </pre>
 * <p>Step #3 - Create the SDO Metadata from an XML Schema</p>
 * <pre>
 * jaxbHelperContext.getXSDHelper().define(xmlSchema);
 * </pre>
 *
 * <p>
 * The JAXBHelperContext allows you to convert between POJOs and DataObjects using
 * a wrap operation.
 * <pre>
 * Customer customer = new Customer();
 * Address address new Address();
 * address.setStreet("123 Any Street");
 * customer.set(address);
 *
 * DataObject customerDO = jaxbHelperContext.wrap(customer);
 * customerDO.getString("address/street");  // returns "123 Any Street"
 * </pre>
 *
 * <p>
 * The JAXBHelperContext allows you to convert between DataObjects and POJOs using
 * an unwrap operation.
 * <pre>
 * Type customerType = jaxbHelperContext.getType(Customer.class);
 * DataObject customerDO = jaxbHelperContext.getDataFactory().create(customerType);
 * customerDO.set("first-name", "Jane");
 *
 * Customer customer = jaxbHelperContext.unwrap(customerDO);
 * customer.getFirstName();  // returns "Jane"
 * </pre>
 *
 * <p>
 * Of course the POJOs may be JPA entities.  Below is an example of wrapping the
 * results of a JPA query.
 * <pre>
 * EntityManagerFactory emf = Persistence.createEntityManagerFactory("CustomerExample");
 * EntityManager em = emf.createEntityManager();
 * {@literal List<MyEntity>} entities = em.createQuery("SELECT e FROM MyEntity e WHERE ...").getResultList();
 * {@literal List<DataObject>} dataObjects = hc.wrap(entities);
 * </pre>
 */
public class JAXBHelperContext extends SDOHelperContext {

    private org.eclipse.persistence.jaxb.JAXBContext jaxbContext;
    private Map<Object, SDODataObject> wrapperDataObjects;

    /**
     * Create a new instance of JAXBHelperContext
     * @param aJAXBContext - The JAXBContext representing the class to XML schema mapping.
     */
    public JAXBHelperContext(JAXBContext aJAXBContext) {
        this(aJAXBContext, Thread.currentThread().getContextClassLoader());
    }

    /**
     * Create a new instance of JAXBHelperContext
     * @param aJAXBContext - The JAXBContext representing the class to XML schema mapping.
     * @param aClassLoader - The ClassLoader containing the generated SDO classes/interfaces (if any).
     */
    public JAXBHelperContext(JAXBContext aJAXBContext, ClassLoader aClassLoader) {
        super(aClassLoader);
        wrapperDataObjects = new IdentityWeakHashMap<Object, SDODataObject>();
        jaxbContext = (org.eclipse.persistence.jaxb.JAXBContext) aJAXBContext;
    }

    /**
     * Override the default helpers/factories, replacing them
     * with JAXB aware ones where necessary.
     */
    @Override
    protected void initialize(ClassLoader aClassLoader)  {
        copyHelper = new SDOCopyHelper(this);
        dataFactory = new JAXBDataFactory(this);
        dataHelper = new SDODataHelper(this);
        equalityHelper = new SDOEqualityHelper(this);
        xmlHelper = new JAXBXMLHelper(this, aClassLoader);
        typeHelper = new SDOTypeHelperDelegate(this);
        xsdHelper = new SDOXSDHelperDelegate(this);
    }

    /**
     * Return the JAXBContext.  The JAXBContext represents the Java
     * class to XML schema information for the POJOs.
     */
    public JAXBContext getJAXBContext() {
        return jaxbContext;
    }

    /**
     * Return the SDO type corresponding to the wrapped class.
     * <pre>
     * Type customerType = jaxbHelperContext.getType(Customer.class);
     * DataObject customerDO = jaxbHelperContext.getDataFactory().create(customerType);
     * </pre>
     */
    public Type getType(Class entityClass) {
        if(null == entityClass) {
            return null;
        }

        XMLDescriptor entityDescriptor = null;
        try {
            entityDescriptor = (XMLDescriptor) jaxbContext.getXMLContext().getSession(entityClass).getDescriptor(entityClass);
        } catch(Exception e) {
            return null;
        }

        XMLSchemaReference schemaReference = entityDescriptor.getSchemaReference();
        if(null == schemaReference) {
            throw SDOException.sdoJaxbNoSchemaReference(entityClass);
        }

        QName qName = schemaReference.getSchemaContextAsQName(entityDescriptor.getNamespaceResolver());
        if(null == qName) {
            throw SDOException.sdoJaxbNoSchemaContext(entityClass);
        }

        Type wrapperType;
        if(entityDescriptor.getSchemaReference().getType() == XMLSchemaReference.COMPLEX_TYPE) {
            wrapperType = getTypeHelper().getType(qName.getNamespaceURI(), qName.getLocalPart());
        } else {
            Property property = getXSDHelper().getGlobalProperty(qName.getNamespaceURI(), qName.getLocalPart(), true);
            wrapperType = property.getType();
        }
        if(null == wrapperType) {
            throw SDOException.sdoJaxbNoTypeForClassBySchemaContext(entityClass, qName);
        }
        return wrapperType;
    }

    /**
     * Return the wrapped class corresponding to the SDO type.
     * <pre>
     * Type customerType = jaxbHelperContext.getTypeHelper().getType("urn:customer", "customer");
     * Class customerClass = jaxbHelperContext.getClass(customerType);
     * </pre>
     */
    public Class getClass(Type type) {
        if(null == type) {
            return null;
        }
        return getObjectDescriptor((SDOType) type).getJavaClass();
    }

    /**
     * Return a DataObject that wraps a POJO.  This call should be
     * made on the root POJO.
     * <pre>
     * Customer customer = new Customer();
     * Address address new Address();
     * address.setStreet("123 Any Street");
     * customer.set(address);
     *
     * DataObject customerDO = jaxbHelperContext.wrap(customer);
     * customerDO.getString("address/street");  // returns "123 Any Street"
     * </pre>
     * Multiple calls to wrap for the same instance POJO return the
     * same instance of DataObject, in other words the following is
     * always true:
     * <pre>
     * jaxbHelperContext.wrap(customer123) == jaxbHelperContext.wrap(customer123)
     * jaxbHelperContext.wrap(customer123) != jaxbHelperContext.wrap(customer456)
     * </pre>
     */
    public DataObject wrap(Object entity) {
        if(null == entity) {
            return null;
        }
        SDODataObject wrapperDO = wrapperDataObjects.get(entity);
        if(null != wrapperDO) {
            return wrapperDO;
        }

        Type wrapperType = getType(entity.getClass());
        if(null == wrapperType) {
           throw SDOException.sdoJaxbNoTypeForClass(entity.getClass());
        }
        wrapperDO = (SDODataObject) getDataFactory().create(wrapperType);

        JAXBValueStore jaxbValueStore = new JAXBValueStore(this, entity);
        wrapperDO._setCurrentValueStore(jaxbValueStore);
        jaxbValueStore.initialize(wrapperDO);

        wrapperDataObjects.put(entity, wrapperDO);
        return wrapperDO;
    }

    /**
     * Helper method that configures container information.
     */
    DataObject wrap(Object entity, Property containmentProperty, DataObject container) {
        SDODataObject sdoDataObject = (SDODataObject) wrap(entity);
        if(null == container) {
            sdoDataObject._setContainmentPropertyName(null);
        } else {
            sdoDataObject._setContainmentPropertyName(containmentProperty.getName());
        }
        sdoDataObject._setContainer(container);
        return sdoDataObject;
    }

    /**
     * Perform the wrap operation on each of the POJOs in the collection,
     * and return the results as a List.
     */
    public List<DataObject> wrap(Collection<Object> entities) {
        return wrap(entities, null, null);
    }

    /**
     * Helper method that configures container information.
     */
    List<DataObject> wrap(Collection<Object> entities, Property containmentProperty, DataObject container) {
        if(null == entities) {
            return new ArrayList<DataObject>(0);
        }
        List<DataObject> dataObjects = new ArrayList<DataObject>(entities.size());
        for(Object entity: entities) {
            dataObjects.add(wrap(entity, containmentProperty, container));
        }
        return dataObjects;
    }

    /**
     * Return the POJO that is wrapped by the DataObject.
     * <pre>
     * Type customerType = jaxbHelperContext.getType(Customer.class);
     * DataObject customerDO = jaxbHelperContext.getDataFactory().create(customerType);
     * DataObject addressDO = customerDO.create("address");
     * addressDO.set("street", "123 Any Street");
     *
     * Customer customer = (Customer) jaxbHelperContext.unwrap(customerDO);
     * customer.getAddress().getStreet();  // returns "123 Any Street"
     * </pre>
     * Multiple calls to unwrap for the same DataObject must return the
     * same instance of Object, in other words the following is always true:
     * <pre>
     * jaxbHelperContext.unwrap(customerDO123) == jaxbHelperContext.unwrap(customerDO123)
     * jaxbHelperContext.unwrap(customerDO123) != jaxbHelperContext.unwrap(customerDO456)
     * customer123 == jaxbHelperContext.unwrap(jaxbHelperContext.wrap(customer123))
     * </pre>
     */
    public Object unwrap(DataObject dataObject) {
        try {
            if(null == dataObject) {
                return null;
            }
            SDODataObject sdoDataObject = (SDODataObject) dataObject;
            JAXBValueStore jpaValueStore = (JAXBValueStore) sdoDataObject._getCurrentValueStore();
            return jpaValueStore.getEntity();
        } catch(ClassCastException e) {
            return null;
        }
    }

    /**
     * Perform the unwrap operation on each of the DataObjects in the collection,
     * and return the results as a List.
     */
    public List<Object> unwrap(Collection<DataObject> dataObjects) {
        if(null == dataObjects) {
            return new ArrayList<Object>(0);
        }
        List<Object> entities = new ArrayList<Object>(dataObjects.size());
        for(DataObject dataObject: dataObjects) {
            entities.add(unwrap(dataObject));
        }
        return entities;
    }

    /**
     * Maintain an association between this POJO and DataObject.
     */
    void putWrapperDataObject(Object anObject, SDODataObject aDataObject) {
        wrapperDataObjects.put(anObject, aDataObject);
    }

    /**
     * Get the XML descriptor for the entity class corresponding to the SDO type.
     */
    XMLDescriptor getObjectDescriptor(SDOType sdoType) {
        QName xsdQName = sdoType.getXsdType();
        if(null == xsdQName) {
            xsdQName = sdoType.getQName();
        }
        XPathFragment xPathFragment = new XPathFragment(xsdQName.getLocalPart());
        xPathFragment.setNamespaceURI(xsdQName.getNamespaceURI());
        XMLDescriptor xmlDescriptor = jaxbContext.getXMLContext().getDescriptorByGlobalType(xPathFragment);
        if (null == xmlDescriptor) {
            xmlDescriptor = jaxbContext.getXMLContext().getDescriptor(xsdQName);
            if (null == xmlDescriptor) {
                throw SDOException.sdoJaxbNoDescriptorForType(sdoType.getQName(), xsdQName);
            }
        }
        return xmlDescriptor;
    }

}
