/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2017 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://oss.oracle.com/licenses/CDDL+GPL-1.1
 * or LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.xml.ws.db.sdo;

import commonj.sdo.DataObject;
import commonj.sdo.Type;
import commonj.sdo.helper.DataFactory;
import commonj.sdo.helper.HelperContext;
import commonj.sdo.helper.XMLDocument;
import org.eclipse.persistence.sdo.SDOConstants;
import org.eclipse.persistence.sdo.helper.SDODataHelper;
import org.eclipse.persistence.sdo.helper.SDOXMLHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.ContentHandler;

import com.sun.xml.ws.util.xml.XmlUtil;
import com.sun.xml.ws.spi.db.BindingContext;
import com.sun.xml.ws.spi.db.TypeInfo;
import com.sun.xml.ws.spi.db.XMLBridge;

import javax.xml.bind.JAXBException;
import javax.xml.bind.attachment.AttachmentMarshaller;
import javax.xml.bind.attachment.AttachmentUnmarshaller;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.stax.StAXSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.logging.Level;
import java.util.logging.Logger;

public class SDOBond<T> implements XMLBridge<T> {

    private static final String CLASSNAME = SDOBond.class.getName();

    private static final Logger logger = Logger.getLogger(CLASSNAME);

    private TypeInfo ti;
    private QName xmlTag = null;
    private Class<T> javaType = null;
    private Type theType = null;
    private SDOContextWrapper parent;
    TransformerFactory tf = XmlUtil.newTransformerFactory(false);
    DocumentBuilderFactory dbf = XmlUtil.newDocumentBuilderFactory(false);

    public SDOBond(SDOContextWrapper parent, TypeInfo ti) {
        this.parent = parent;
        this.ti = ti;
        this.javaType = (Class<T>) ti.type;
        this.xmlTag = ti.tagName;
        HelperContext context = parent.getHelperContext();
        this.theType = context.getTypeHelper().getType(javaType);
    }

//    public SDOBond(Class<T> type, QName xml) {
//        logger.entering("SDOBond", "constructor");
//        javaType = type;
//        xmlTag = xml;
//        HelperContext context = parent.getHelperContext();
//        this.theType = context.getTypeHelper().getType(javaType);      
//    }

    public QName getXmlTag() {
        return xmlTag;
    }

    public Class<T> getJavaType() {
        return javaType;
    }

    private T deserialize(Source src, javax.xml.bind.attachment.AttachmentUnmarshaller au) {
        try {
            if (!commonj.sdo.DataObject.class.isAssignableFrom(javaType) && !javaType.isInterface()) {
                return (T) deserializePrimitives(src);
            }
            HelperContext context = parent.getHelperContext();
            SDOAttachmentUnmarshaller unmarshaller = null;
            if (au != null) {
                unmarshaller = new SDOAttachmentUnmarshaller(au);
            }

            DataFactory dataFactory = context.getDataFactory();
            DataObject loadOptions = dataFactory.create(SDOConstants.ORACLE_SDO_URL, SDOConstants.XMLHELPER_LOAD_OPTIONS);
            //bug 8680450
            loadOptions.set(SDOConstants.TYPE_LOAD_OPTION, theType);
            if (unmarshaller != null) {
                loadOptions.set(SDOConstants.ATTACHMENT_UNMARSHALLER_OPTION, unmarshaller);
            }
            XMLDocument xdoc = context.getXMLHelper().load(src, null, loadOptions);
            DataObject obj = xdoc.getRootObject();
            Object o = SDOUtils.unwrapPrimitives(obj);
            return (T) o;  // ClassCast possible without check
        }
        catch (Exception e) {
            throw new SDODatabindingException(e);
        }
    }

    private Object deserializePrimitives(Source src) throws Exception {
        if (javaType == null) {
            return null;
        }
        String value = null;
        if (src instanceof StAXSource) {
            StAXSource staxSrc = (StAXSource)src;
            XMLStreamReader xr = staxSrc.getXMLStreamReader();
            if(xr.isStartElement()) {
                xr.next();
            }
            StringBuilder sb = new StringBuilder();
            while(xr.isCharacters()) {
                sb.append(xr.getText());
                xr.next();
            }
            value = sb.toString().trim();
        } else {
            DOMResult result = new DOMResult();
            Transformer t = tf.newTransformer();
            t.transform(src, result);
            value = ((Document)result.getNode()).getDocumentElement().getTextContent().trim(); //xmlElement.getTextContent().trim();
        }
        if (value == null) {
            return null;
        }
        Object o = null;
        try {
            o = ((SDODataHelper) parent.getHelperContext().getDataHelper()).convertFromStringValue(value, theType);
        } catch (Exception e) {
            // content class does not accept null or empty value, such as BigDecimal, Integer etc
            // these type of empty value will cause toplink data helper to fail, workaround to prevent such failures
            if (value.length() == 0) {
                if (logger.isLoggable(Level.FINEST)) {
                    logger.finest("Deserialized primitive part has 0 length text, result is null");
                }
                return null;
            }
        }

        if (logger.isLoggable(Level.FINEST)) {
            logger.log(Level.FINEST, "Deserialized primitive part {0}", o);
        }
        return o;
    }

    private String serializePrimitive(Object obj, Class<?> contentClass) {
        if (logger.isLoggable(Level.FINEST)) {
            logger.log(Level.FINEST, "Primitive class to be serialized ==> {0}", contentClass);
        }
        HelperContext context = parent.getHelperContext();
        Type type = context.getTypeHelper().getType(contentClass);
        if (type != null) {
            return ((SDODataHelper) context.getDataHelper()).convertToStringValue(obj, type);
        }

        if (logger.isLoggable(Level.FINE)) {
            logger.log(Level.FINE, "Invalid SDO primitive type: {0}", contentClass.getClass().getName());
        }
        throw new SDODatabindingException("Invalid SDO primitive type: "
                + contentClass.getClass().getName());
    }

    private void serializeToResult(String value, Result result) {
        try {
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.newDocument();
            Element elt = doc.createElementNS(xmlTag.getNamespaceURI(), "ns1:"
                    + xmlTag.getLocalPart());
            doc.appendChild(elt);
            elt.appendChild(doc.createTextNode(value));
            DOMSource ds = new DOMSource(elt);
            Transformer t = tf.newTransformer();
            t.transform(ds, result);
        } catch (Exception e) {
            throw new SDODatabindingException(e.getMessage());
        }
    }

    @Override
    public BindingContext context() {
        return parent;
    }

    private void serializeDataObject(DataObject java, Result result,
            javax.xml.bind.attachment.AttachmentMarshaller am) {
        logger.entering(CLASSNAME, "serializeDataObject");
        try {
            HelperContext context = parent.getHelperContext();
            SDOAttachmentMarshaller marshaller = null;
            if (am != null) {
                marshaller = new SDOAttachmentMarshaller(am);
            }

            // check Primitives for T
            SDOXMLHelper sdoXMLHelper = (SDOXMLHelper) context.getXMLHelper();

            // Bug 8909750 - Toplink already sets this to "GMT".  ADF
            // resets it before we get here, so don't change it again.
            //sdoXMLHelper.setTimeZone(TimeZone.getTimeZone("GMT"));
            sdoXMLHelper.setTimeZoneQualified(true);

            XMLDocument xmlDoc = sdoXMLHelper.createDocument((DataObject) java, xmlTag.getNamespaceURI(), xmlTag.getLocalPart());
            if (xmlDoc == null) {
                return;
            }
            xmlDoc.setXMLDeclaration(false);
            DataObject saveOptions = null;
            if (marshaller != null) {
                DataFactory dataFactory = parent.getHelperContext().getDataFactory();
                saveOptions = dataFactory.create(SDOConstants.ORACLE_SDO_URL,
                    SDOConstants.XMLHELPER_LOAD_OPTIONS);
                saveOptions.set(SDOConstants.ATTACHMENT_MARSHALLER_OPTION, marshaller);
            }
            sdoXMLHelper.save(xmlDoc, result, saveOptions);

        } catch (Exception e) {
            throw new SDODatabindingException(e);
        }
    }

    @Override
    public void marshal(T object, XMLStreamWriter output,
            AttachmentMarshaller am) throws JAXBException {
        /*  Didn't work due to bug 8539542
        StAXResult result = new StAXResult(writer);
        sdoXMLHelper.save(xmlDoc, result, null);
        */
        try {
            output.flush();
        } catch (Exception e) {
            e.printStackTrace();
        }

        SAX2StaxContentHandler handler = new SAX2StaxContentHandler(output);
        if (object instanceof DataObject) {
            serializeDataObject((DataObject) object, new SAXResult(handler),
                    am);
            return;
        }

       try {
            String value = serializePrimitive(object, javaType);
            String prefix = output.getPrefix(xmlTag.getNamespaceURI());
            //TODO, this is a hack, seems to be wrong. why should namespace returned  is ""?
            if (xmlTag.getNamespaceURI().equals("")) {
                output.writeStartElement("", xmlTag.getLocalPart(), xmlTag.getNamespaceURI());
//            } else if (prefix == null) {
//                output.writeStartElement(xmlTag.getNamespaceURI(), xmlTag.getLocalPart());
            } else {
                output.writeStartElement(prefix, xmlTag.getLocalPart(), xmlTag.getNamespaceURI());
                output.writeNamespace(prefix, xmlTag.getNamespaceURI());
            }
            output.writeCharacters(value);
            output.writeEndElement();
        } catch (XMLStreamException e) {
            throw new SDODatabindingException(e);
        }
    }

    @Override
    public void marshal(T object, OutputStream output,
            NamespaceContext nsContext, AttachmentMarshaller am)
            throws JAXBException {
        if (object instanceof DataObject) {
            serializeDataObject((DataObject) object, new StreamResult(output),
                    am);
            return;
        }

        try {
            String value = serializePrimitive(object, javaType);
            String prefix = nsContext.getPrefix(xmlTag.getNamespaceURI());
            StringBuilder sb = new StringBuilder();
            if ("".equals(prefix)) {
                sb.append("<").append(xmlTag.getLocalPart()).append(">");
                sb.append(value);
                sb.append("</").append(xmlTag.getLocalPart()).append(">");
            } else if (prefix != null) {
                sb.append("<").append(prefix).append(":")
                        .append(xmlTag.getLocalPart()).append(">");
                sb.append(value);
                sb.append("</").append(prefix).append(":")
                        .append(xmlTag.getLocalPart()).append(">");
            } else {
                // Unbound namespace!
                sb.append("<ns1:").append(xmlTag.getLocalPart())
                        .append(" xmlns:ns1=\"")
                        .append(xmlTag.getNamespaceURI()).append("\"")
                        .append(">");
                sb.append(value);
                sb.append("</ns1:").append(xmlTag.getLocalPart()).append(">");
            }
        } catch (Exception e) {
            throw new SDODatabindingException(e);
        }
    }

    @Override
    public void marshal(T object, Node output) throws JAXBException {
        Result res = new DOMResult(output);
        if (object instanceof DataObject) {
            serializeDataObject((DataObject) object, res, null);
            return;
        }
        String value = serializePrimitive(object, javaType);
        serializeToResult(value, res);
    }

    @Override
    public void marshal(T object, ContentHandler contentHandler,
            AttachmentMarshaller am) throws JAXBException {
        Result res = new SAXResult(contentHandler);
        if (object instanceof DataObject) {
            serializeDataObject((DataObject) object, res, null);
            return;
        }

        String value = serializePrimitive(object, javaType);
        serializeToResult(value, res);
    }

    @Override
    public void marshal(T object, Result result) throws JAXBException {
        if (object instanceof DataObject) {
            serializeDataObject((DataObject) object, result, null);
            return;
        }

        String value = serializePrimitive(object, javaType);
        serializeToResult(value, result);
    }

    @Override
    public T unmarshal(XMLStreamReader in, AttachmentUnmarshaller au)
            throws JAXBException {
        return deserialize(new StAXSource(in), au);
    }

    @Override
    public T unmarshal(Source in, AttachmentUnmarshaller au)
            throws JAXBException {
        return deserialize(in, au);
    }

    @Override
    public T unmarshal(InputStream in) throws JAXBException {
        return deserialize(new StreamSource(in), null);
    }

    @Override
    public T unmarshal(Node n, AttachmentUnmarshaller au) throws JAXBException {
        return deserialize(new DOMSource(n), au);
    }

    @Override
    public TypeInfo getTypeInfo() {
        return ti;
    }

    @Override
    public boolean supportOutputStream() {
        return true;
    }

}
