/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2013-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 com.sun.xml.ws.util.xml.XmlUtil;
import commonj.sdo.DataObject;
import commonj.sdo.Property;
import commonj.sdo.Type;
import commonj.sdo.impl.HelperProvider;
import commonj.sdo.helper.HelperContext;
import commonj.sdo.helper.TypeHelper;
import commonj.sdo.helper.XMLDocument;
import commonj.sdo.helper.XSDHelper;

import org.eclipse.persistence.sdo.SDOType;
import org.eclipse.persistence.sdo.helper.SDOXMLHelper;
import org.eclipse.persistence.sdo.helper.SDOXSDHelper;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.helpers.NamespaceSupport;

import javax.xml.XMLConstants;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;

/**
 * A set of core utility methods that shapes the sdo databinding
 * Created by IntelliJ IDEA.
 * User: giglee
 * Date: May 13, 2009
 * Time: 10:21:12 AM
 * To change this template use File | Settings | File Templates.
 */
public class SDOUtils {

    public static final String NS_XMLNS = "http://www.w3.org/2001/XMLSchema";
    public static final String NS_WSDL = "http://schemas.xmlsoap.org/wsdl/";
    private static final String NS_XSD = "http://www.w3.org/2001/XMLSchema";
    private static final QName SCHEMA_INCLUDE_QNAME = new QName(NS_XSD, "include");
    private static final QName SCHEMA_IMPORT_QNAME = new QName(NS_XSD, "import");
    public static final QName QNAME_SCHEMA = new QName(NS_XMLNS, "schema");

    static volatile TransformerFactory transformerFactory = null;
    static volatile DocumentBuilderFactory dbf = null;

    public static Transformer newTransformer() {
        if (transformerFactory == null) {
            synchronized(SDOUtils.class) {
                if (transformerFactory == null) {
                    transformerFactory = XmlUtil.newTransformerFactory(false);
                }
            }
        }
        try {
            return transformerFactory.newTransformer();
        } catch (TransformerConfigurationException e) {
            throw new RuntimeException(e);
        }
    }

    public static DocumentBuilder newDocumentBuilder() {
        if (dbf == null) {
            synchronized(SDOUtils.class) {
                if (dbf == null) {
                    DocumentBuilderFactory tmpDBF = XmlUtil.newDocumentBuilderFactory(false);
                    tmpDBF.setValidating(false);
                    tmpDBF.setNamespaceAware(true);
                    tmpDBF.setIgnoringElementContentWhitespace(true);
                    tmpDBF.setIgnoringComments(true);
                    dbf = tmpDBF;
                }
            }
        }
        try {
            DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
            return documentBuilder;
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Serialize a DataObject to the specified element
     * Per bug  6120620,, we use only GMT timezone
     */
    public static Element sdoToDom(HelperContext hc, DataObject obj, String targetNamespace, String localName)
            throws ParserConfigurationException, IOException {
        SDOXMLHelper sdoXMLHelper = (SDOXMLHelper) hc.getXMLHelper();

        // Removed this from JRF for ADF use case.
        //sdoXMLHelper.setTimeZone(TimeZone.getTimeZone("GMT"));

        sdoXMLHelper.setTimeZoneQualified(true);

        XMLDocument xmlDoc = sdoXMLHelper.createDocument(obj, targetNamespace, localName);
        if (xmlDoc == null) {
            return null;
        }

        Document doc = newDocumentBuilder().newDocument();
        DOMResult result = new DOMResult(doc);
        sdoXMLHelper.save(xmlDoc, result, null);
        return ((Document) result.getNode()).getDocumentElement();
    }

    /**
     * Serialize a DataObject to the specified xml element in text xml
     *
     * @param hc
     * @param obj
     * @param targetNamespace
     * @param localName
     * @return
     * @throws ParserConfigurationException
     * @throws IOException
     */
    public static Source sdoToXML(HelperContext hc, DataObject obj, String targetNamespace, String localName)
            throws ParserConfigurationException, IOException {
        SDOXMLHelper sdoXMLHelper = (SDOXMLHelper) hc.getXMLHelper();
        sdoXMLHelper.setTimeZone(TimeZone.getTimeZone("GMT"));
        sdoXMLHelper.setTimeZoneQualified(true);

        XMLDocument xmlDoc = sdoXMLHelper.createDocument(obj, targetNamespace, localName);
        if (xmlDoc == null) {
            return null;
        }

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        StreamResult result = new StreamResult(bout);

        sdoXMLHelper.save(xmlDoc, result, null);
        byte[] bytes = bout.toByteArray();
        System.out.println("data obj converted to xml: " + new String(bytes));
        return new StreamSource(new ByteArrayInputStream(bytes));
    }

    /**
     * Register the types defined in the given schema with the given sdo helper context
     *
     * @param context
     * @param schemas
     */
    public static void registerSDOContext(HelperContext context, List<Source> schemas) {
        SDOXSDHelper xsdHelper = (SDOXSDHelper) context.getXSDHelper();
        SDODatabindingSchemaResolver schemaResolver = new SDODatabindingSchemaResolver(schemas);
        for (Source source : schemas) {
            //SDOUtils.printDOM(source);
            xsdHelper.define(source, schemaResolver);
        }
    }


    public static List<Source> getSchemaClosureFromWSDL(Source wsdlSource) {
        String systemId = wsdlSource.getSystemId();
        Document wsdl = createDOM(wsdlSource);
        List<Source> list = new ArrayList<Source>();
        addSchemaFragmentSource(wsdl, systemId, list);
        return list;
    }

    public static Document createDOM(Source wsdlSource) {
        Transformer trans = newTransformer();
        DocumentBuilder builder = newDocumentBuilder();
        Document doc = builder.newDocument();
        DOMResult result = new DOMResult(doc);
        try {
            trans.transform(wsdlSource, result);
        } catch (TransformerException te) {
            throw new RuntimeException(te);
        }
        return (Document) result.getNode();
    }

    public static Map<String, Source> getMetadataClosure(List<Source> schemas) {
        Map<String, Source> closureDocs = new HashMap<String, Source>();
        Map<String, Source> currentDocs = new HashMap<String, Source>();
        Set<String> remaining = new HashSet<String>();
        for (Source src : schemas) {
            currentDocs.put(src.getSystemId(), src);
        }

        remaining.addAll(currentDocs.keySet());

        while (!remaining.isEmpty()) {
            Iterator<String> it = remaining.iterator();
            String current = it.next();
            remaining.remove(current);
            Source currentDoc = currentDocs.get(current);
            if (currentDoc == null) {
                currentDoc = loadSourceFromURL(current);
            }

            Set<String> imports = new HashSet<String>();
            currentDoc = getImports(currentDoc, imports);
            closureDocs.put(current, currentDoc);
            for (String importedDoc : imports) {
                if (closureDocs.get(importedDoc) == null) {
                    remaining.add(importedDoc);
                }
            }
        }
        return closureDocs;
    }

    @SuppressWarnings("CallToThreadDumpStack")
    private static Source loadSourceFromURL(String systemID) {
        Source targetXSD;
        try {
            URL targetURL = new URL(systemID);
            InputStream is = targetURL.openStream();
            targetXSD = new StreamSource(is);
            targetXSD.setSystemId(targetURL.toExternalForm());
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
        return targetXSD;
    }

    private static Source getImports(Source currentDoc, Set<String> importedDocs) {
        Document doc = createDOM(currentDoc);
        Element root = doc.getDocumentElement();

        try {
            NodeList imports = root.getElementsByTagNameNS(SCHEMA_INCLUDE_QNAME.getNamespaceURI(), SCHEMA_INCLUDE_QNAME.getLocalPart());
            NodeList includes = root.getElementsByTagNameNS(SCHEMA_IMPORT_QNAME.getNamespaceURI(), SCHEMA_INCLUDE_QNAME.getLocalPart());

            if (imports != null) {
                for (int i = 0; i < imports.getLength(); i++) {
                    Element e = (Element) imports.item(i);
                    String importedDoc = e.getAttributeNS(NS_XMLNS, "schemaLocation");
                    if (importedDoc != null) {
                        URL u = new URL(currentDoc.getSystemId());
                        importedDocs.add(new URL(u, importedDoc).toString());
                    }
                }
            }
            if (includes != null) {
                for (int i = 0; i < imports.getLength(); i++) {
                    Element e = (Element) imports.item(i);
                    String importedDoc = e.getAttributeNS(NS_XMLNS, "schemaLocation");
                    if (importedDoc != null) {
                        URL u = new URL(currentDoc.getSystemId());
                        importedDocs.add(new URL(u, importedDoc).toString());
                    }
                }
            }
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
        return new DOMSource(doc, currentDoc.getSystemId());
    }


    private static void addSchemaFragmentSource(Document doc, String systemId, List<Source> list) {

        Element e = doc.getDocumentElement();
        assert e.getNamespaceURI().equals(NS_WSDL);
        assert e.getLocalName().equals("definitions");

        NodeList typesList = e.getElementsByTagNameNS(NS_WSDL, "types");
        for (int i = 0; i < typesList.getLength(); i++) {
            NodeList schemaList =
                    ((Element) typesList.item(i)).getElementsByTagNameNS(NS_XMLNS, "schema");
            for (int j = 0; j < schemaList.getLength(); j++) {
                Element elem = (Element) schemaList.item(j);
                NamespaceSupport nss = new NamespaceSupport();
                buildNamespaceSupport(nss, elem);
                patchDOMFragment(nss, elem);
                list.add(new DOMSource(elem, systemId + "#schema" + j));
            }
        }
    }

    private static void buildNamespaceSupport(NamespaceSupport nss, Node node) {
        if (node == null || node.getNodeType() != Node.ELEMENT_NODE) {
            return;
        }

        buildNamespaceSupport(nss, node.getParentNode());

        nss.pushContext();
        NamedNodeMap atts = node.getAttributes();
        for (int i = 0; i < atts.getLength(); i++) {
            Attr a = (Attr) atts.item(i);
            if ("xmlns".equals(a.getPrefix())) {
                nss.declarePrefix(a.getLocalName(), a.getValue());
                continue;
            }
            if ("xmlns".equals(a.getName())) {
                nss.declarePrefix("", a.getValue());
                continue;
            }
        }
    }

    private static void patchDOMFragment(NamespaceSupport nss, Element elem) {
        NamedNodeMap atts = elem.getAttributes();
        for (Enumeration en = nss.getPrefixes(); en.hasMoreElements();) {
            String prefix = (String) en.nextElement();

            for (int i = 0; i < atts.getLength(); i++) {
                Attr a = (Attr) atts.item(i);
                if (!"xmlns".equals(a.getPrefix()) || !a.getLocalName().equals("prefix")) {
                    elem.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + prefix, nss.getURI(prefix));
                }
            }
        }
    }


    public static Object unwrapPrimitives(Object obj) {
        if (obj == null) {
            return obj;
        }
        if (!(obj instanceof DataObject)) {
            return obj;
        }
        DataObject dataObject = (DataObject) obj;
        if (dataObject.getClass().getName().endsWith("WrapperImpl")) {
            if (dataObject.get(0) != null) {
                System.out.println("unwrapped object: " + dataObject.get(0).getClass().getName());
            }
            return dataObject.get(0);
        }

        return obj;
    }


    @SuppressWarnings("CallToThreadDumpStack")
    public static void printDOM(Source src) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            StreamResult sr = new StreamResult(bos);
            TransformerFactory tf = XmlUtil.newTransformerFactory(false);
            Transformer trans = tf.newTransformer();
            trans.transform(src, sr);
            System.out.println("**********\n" + bos.toString());
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static String dom2String(DOMSource domSrc) throws TransformerConfigurationException, TransformerException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        StreamResult sr = new StreamResult(bos);
        TransformerFactory tf = XmlUtil.newTransformerFactory(false);
        Transformer trans = tf.newTransformer();
        trans.transform(domSrc, sr);
        return sr.toString();
    }

    // note only use for debugging, once the reader is read, the cursor can't be set back
    @SuppressWarnings("CallToThreadDumpStack")
    public static void printXMLReader(XMLStreamReader xml) {
          try {
            Document doc = SDOUtils.newDocument();
            SAX2DOMContentHandler handler = new SAX2DOMContentHandler(doc);
            Stax2SAXAdapter adapter = new Stax2SAXAdapter(xml, false);
            adapter.parse(handler);
            printDOM(new DOMSource(doc));
          } catch (Exception e) {
              e.printStackTrace();
          }
    }



    /**
     * get the element name represented by this property
     * @param context
     * @param p
     * @return
     */
    public static QName getPropertyElementName(HelperContext context, Property p) {
        XSDHelper helper = context.getXSDHelper();
        String localName = p.getName();
        String ns = helper.getNamespaceURI(p);
        return new QName(ns, localName);
    }


    // used by tests
    public static List defineSchema(HelperContext hc, File f) throws Exception {
        FileInputStream fin = null;
        try {
            fin = new FileInputStream(f);
            InputStreamReader reader = new InputStreamReader(fin);
            StreamSource source = new StreamSource(reader);
            return ((SDOXSDHelper) hc.getXSDHelper()).define(source, null);
        }
        finally {
            if (fin != null) {
                try {
                    fin.close();
                } catch (IOException ioe) {
                }
            }
        }
    }


    public static Document newDocument() {
        return newDocumentBuilder().newDocument();
    }

    /**
     * Check whether a java class is supported
     * The builtin type includes all the default type mappings specified in the SDO Spec
     * @param javaType
     * @param qname
     * @return
     */
    public static boolean validateBuiltinType(String javaType, QName qname) {
        return validateSupportedType(HelperProvider.getDefaultContext(), javaType, qname);
    }

    public static boolean validateSupportedType(HelperContext hc, String javaType, QName qname) {
        TypeHelper typeHelper = hc.getTypeHelper();
        if (qname != null) {
            Type type = typeHelper.getType(qname.getNamespaceURI(), qname.getLocalPart());
            String java = ((SDOType) type).getInstanceClassName();
            if (java != null) {
                return java.equals(javaType);
            }
            return false;
        } else {
            if (isPrimitive(javaType)) {
                return true;
            }
            try {
                Class cls = Thread.currentThread().getContextClassLoader().loadClass(javaType);
                Type type = typeHelper.getType(cls);
                return type == null ? false : true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
    }

    // all primitives listed here is supported
    private static boolean isPrimitive(String type) {
        if (type.equals("int") ||
                type.equals("short") ||
                type.equals("long") ||
                type.equals("byte") ||
                type.equals("float") ||
                type.equals("double") ||
                type.equals("boolean")) {
            return true;
        }
        // the rest we will let toplink handle it
        return false;
    }


    public static Set<SchemaInfo> getSchemas(File f) throws Exception {
        FileInputStream fin = null;
        try {
            fin = new FileInputStream(f);
            StreamSource source = new StreamSource(fin);
            source.setSystemId(f.toURL().toExternalForm());
            List<Source> main_schema = new ArrayList<Source>();
            main_schema.add(source);
            Map<String, Source> map = SDOUtils.getMetadataClosure(main_schema);

            Set<SchemaInfo> schemas = new HashSet<SchemaInfo>();
            for (Map.Entry<String, Source> entry : map.entrySet()) {
                SchemaInfo info = new SchemaInfo(entry.getKey(), null, entry.getValue());
                schemas.add(info);
            }
            return schemas;
        }
        finally {
            if (fin != null) {
                fin.close();
            }
        }
    }


    public static Set<SchemaInfo> getSchemas(String filePath) throws Exception {
        Set<SchemaInfo> schemas = new HashSet<SchemaInfo>();

        Document document = newDocumentBuilder().parse(new File(filePath));
        Element rootEl = document.getDocumentElement();
        if (QNAME_SCHEMA.equals(new QName(rootEl.getNamespaceURI(), rootEl.getLocalName()))) {
            SchemaInfo info = new SchemaInfo(filePath, null, new DOMSource(rootEl));
            schemas.add(info);
        } else if ("http://schemas.xmlsoap.org/wsdl/".equals(rootEl.getNamespaceURI())) {
            Element types = null;
            Node n = rootEl.getFirstChild();
            while (types == null) {
                if (n instanceof Element && ((Element)n).getLocalName().equals("types")) {
                    types = (Element)n;
                } else {
                    n = n.getNextSibling();
                }
            }
            NodeList nl = types.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++ ) {
                Node x = nl.item(i);
                if (x instanceof Element && ((Element)x).getLocalName().equals("schema")) {
                    SchemaInfo info = new SchemaInfo(filePath, null, new DOMSource((Element)x));
                    schemas.add(info);
                }
            }
//            Definition def = WSDLHelper.readWsdl(url, document);
//            SchemaCollection schemaCollection = SchemaAnalyzer.collectSchemas(def);
//            schemas = SchemaHelper.getSchemaInfos(url.toString(), schemaCollection,true);
//            for (SchemaInfo schema : schemas) {
//                Element schemaElement = schema.getSchemaElement();
//                SchemaHelper.fixMissingNamespaces(schemaElement);
//            }
        } else {
//            throw new IOException("Document found at '" + sdoSchema + "' is not a valid schema/wsdl document.");
        }

        return schemas;
    }
}
