/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *  
 */

package org.teiid.query.mapping.xml;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.teiid.query.QueryPlugin;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


/**
 * <p>Reads an mapping definition file in XML format.  When finished reading
 * the mapping info from the XML file, this class holds an object representation
 * of it, rooted in a <code>MappingNode</code> instance, and also holds an XML
 * <code>Document</code> representation. </p>
 *
 * <h3>Example usage (loading from stream; exceptions not shown)</h3>
 * <p><pre>
 *     MappingLoader loader = new MappingLoader();
 *     MappingNode mappingRootNode = loader.loadDocument(istream);
 * </pre></p>
 *
 * @see MappingNode
 */
public class MappingLoader {
    
    HashMap unresolvedNamespaces = new HashMap();
    
    /**
     * <p>Load mapping definition from XML Document object. </p>
     */
    public MappingDocument loadDocument(InputStream stream) throws MappingException {
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setValidating(false);
            DocumentBuilder db = dbf.newDocumentBuilder();
            Document doc = db.parse(stream);
            return loadContents(doc);
        } catch (IOException e) {
            throw new MappingException(e);
        } catch (ParserConfigurationException e) {
        	throw new MappingException(e);
		} catch (SAXException e) {
			throw new MappingException(e);
		}
    }
    
    public MappingDocument loadDocument(String fileName) throws MappingException, FileNotFoundException {
    	FileInputStream fis = new FileInputStream(fileName);
    	return loadDocument(fis);
    }    

    /**
     * Load contents into temporary structures.
     */
    MappingDocument loadContents(Document document) throws MappingException {
        MappingDocument doc = new MappingDocument(false);
        
        loadDocumentProperties(doc, document.getDocumentElement());
        
        // now load all the children
        Collection mappingChildren = getChildren(document.getDocumentElement(), MappingNodeConstants.Tags.MAPPING_NODE_NAME); 
        doc = (MappingDocument)recursiveLoadContents(mappingChildren, doc);
        return doc;
    }
        
    private MappingNode recursiveLoadContents(Collection mappingChildren, MappingBaseNode parent) 
        throws MappingException {
        
        MappingBaseNode node = null;
        for (Iterator i = mappingChildren.iterator(); i.hasNext();){
            Element elem = (Element)i.next();

            node = processMappingNode(elem, parent);

            Collection childrenMappingNodes = getChildren(elem, MappingNodeConstants.Tags.MAPPING_NODE_NAME);
            recursiveLoadContents(childrenMappingNodes, node);
        }
        return parent;
    }
    
    public Collection<Element> getChildren(Element elem, String name) {
    	NodeList children = elem.getChildNodes();
    	LinkedList<Element> results = new LinkedList<Element>();
    	for (int i = 0; i < children.getLength(); i++) {
    		Node node = children.item(i);
    		if (node instanceof Element && node.getNodeName().equals(name)) {
    			results.add((Element)node);
    		}
    	}
    	return results;
    }
    
    public Element getChild(Element elem, String name) {
    	NodeList children = elem.getChildNodes();
    	for (int i = 0; i < children.getLength(); i++) {
    		Node node = children.item(i);
    		if (node instanceof Element && node.getNodeName().equals(name)) {
    			return (Element)node;
    		}
    	}
    	return null;
    }

    /**
     * Load a "sequence" node from the mapping document
     * @param element - parent element
     * @return a sequence node
     */
    MappingSequenceNode loadSequenceNode(Element element, MappingBaseNode parentNode) {
        MappingSequenceNode node = new MappingSequenceNode();
        node.setMinOccurrs(getMinOccurrences(element));
        node.setMaxOccurrs(getMaxOccurrences(element));        
        node.setSource(getSource(element));
        node.setExclude(isExcluded(element));
        node.setStagingTables(getStagingTableNames(element));
        return node;
    }
    
    /**
     * Load a "Element" node from mapping document. This can be a
     * "Recursive" or "Criteria" or normal node.
     * @param element - parent element
     * @return retuns a MappingElement
     */
    MappingElement loadElementNode(Element element, MappingBaseNode parentNode, boolean rootElement) 
        throws MappingException{
        
        MappingElement node = null;
        
        String name = getName(element);
        if (name == null || name.length()==0) {
            throw new MappingException(QueryPlugin.Util.getString("MappingLoader.invalidName")); //$NON-NLS-1$
        }
        
        Namespace[] namespaces = getNamespaceDeclarations(element);
        Namespace namespace = getNamespace(element, namespaces, parentNode);
        
        // There are effectively three types of elements, recursive, criteria and regular..
        if (isRecursive(element)) {
            // first check if this is a "recursive" element
            MappingRecursiveElement elem = new MappingRecursiveElement(name, namespace, getRecursionMappingClass(element));
            elem.setCriteria(getRecursionCriteria(element));
            elem.setRecursionLimit(getRecursionLimit(element), throwExceptionOnRecursionLimit(element));
            node = elem;
        }
        else {
            // this regular element
            node = new MappingElement(name, namespace);
        }
        
        // now load all other common properties.
        if (rootElement) {
            node.setMinOccurrs(1);
            node.setMaxOccurrs(1);            
        }
        else {
            node.setMinOccurrs(getMinOccurrences(element));
            node.setMaxOccurrs(getMaxOccurrences(element));
        }
        
        node.setNameInSource(getNameInSource(element));
        node.setSource(getSource(element));
        node.setOptional(isOptional(element));
        node.setDefaultValue(getDefaultValue(element));
        node.setValue(getFixedValue(element));
        node.setNillable(isNillable(element));
        node.setExclude(isExcluded(element));
        node.setType(getBuitInType(element));
        node.setNormalizeText(getNormalizeText(element));
        node.setAlwaysInclude(includeAlways(element));
        node.setStagingTables(getStagingTableNames(element));                
        node.setNamespaces(namespaces);

        return node;
    }

    /**
     * Load an attribute node from the element
     */
    void loadAttributeNode(Element element, MappingElement parent) throws MappingException {
        String name = getName(element);
        String nsPrefix = getElementValue(element, MappingNodeConstants.Tags.NAMESPACE_PREFIX);
        
        if (name == null || name.length()==0) {
            throw new MappingException(QueryPlugin.Util.getString("MappingLoader.invalidName")); //$NON-NLS-1$
        }
        
        Namespace namespace = null;
        boolean normalAttribute = true;
        if (name.equalsIgnoreCase(MappingNodeConstants.NAMESPACE_DECLARATION_ATTRIBUTE_NAMESPACE)) {
            // this is default name space where only "xmlns" is defined. We do not need to global map
            // as there may be more(I guess..)
            namespace = new Namespace("", getFixedValue(element)); //$NON-NLS-1$
            parent.addNamespace(namespace);            
            normalAttribute = false;
        }
        else if (nsPrefix != null && nsPrefix.equalsIgnoreCase(MappingNodeConstants.NAMESPACE_DECLARATION_ATTRIBUTE_NAMESPACE)) {
            // prior to 5.5, a document can use the namespaces before it declares them; this little
            // trick(hack) to fix it.
            namespace = (Namespace)this.unresolvedNamespaces.remove(name);
            if (namespace == null) {
                namespace = new Namespace(name);    
            }
            // this is specific name space declaration like xsi, foo etc..                            
            namespace.setUri(getFixedValue(element));
            parent.addNamespace(namespace);      
            normalAttribute = false;
        }
        else {
            // this is a invalid form of 
            namespace = getNamespace(element, null, parent);
        }
        
        // if this not any namespace specific attribute then it is any normal attribute on the
        // tree; treat it as such.
        if (normalAttribute) {
            MappingAttribute attribute = new MappingAttribute(getName(element), namespace);
            // now get all other properties for the attribute.
            attribute.setNameInSource(getNameInSource(element));
            attribute.setDefaultValue(getDefaultValue(element));
            attribute.setValue(getFixedValue(element));
            attribute.setExclude(isExcluded(element));
            attribute.setNormalizeText(getNormalizeText(element));
            attribute.setOptional(isOptional(element));
            attribute.setAlwaysInclude(includeAlways(element));
            parent.addAttribute(attribute);
        }
    }
    
    /**
     * Load a "Choice" Node
     * @param element
     * @return
     */
    MappingChoiceNode loadChoiceNode(Element element, MappingBaseNode parentNode) {
        MappingChoiceNode node = new MappingChoiceNode(exceptionOnDefault(element));
        node.setMinOccurrs(getMinOccurrences(element));
        node.setMaxOccurrs(getMaxOccurrences(element));        
        node.setSource(getSource(element));
        node.setExclude(isExcluded(element));
        node.setStagingTables(getStagingTableNames(element));
        return node;
    }
    
    /**
     * Load the "all" node
     */
    MappingAllNode loadAllNode(Element element, MappingBaseNode parentNode) {
        MappingAllNode node = new MappingAllNode();
        node.setMinOccurrs(getMinOccurrences(element));
        node.setMaxOccurrs(getMaxOccurrences(element));        
        node.setSource(getSource(element));
        node.setExclude(isExcluded(element));
        node.setStagingTables(getStagingTableNames(element));
        return node;        
    }
    
    /**
     * Load the comment node
     */
    void loadCommentNode(Element element, MappingElement parent) {
        MappingCommentNode comment = new MappingCommentNode(getCommentText(element));
        parent.addCommentNode(comment);
    }
    
    /**
     * Load the mapping document
     */
    MappingDocument loadDocumentProperties (MappingDocument doc, Element element) {
        boolean formatted = isFormattedDocument(element);
        if (formatted != MappingNodeConstants.Defaults.DEFAULT_FORMATTED_DOCUMENT.booleanValue()) {
            doc.setFormatted(formatted);
        }
        String encoding = getDocumentEncoding(element);
        if (!MappingNodeConstants.Defaults.DEFAULT_DOCUMENT_ENCODING.equalsIgnoreCase(encoding)) {
            doc.setDocumentEncoding(encoding);
        }
       return doc;
    }

    /**
     * Load a criteria node; Criteria node can only be child of a choice node.
     */
    MappingCriteriaNode loadCriteriaNode(Element element, MappingBaseNode parentNode) throws MappingException{
        if (getCriteria(element) != null || isDefaultOnChoiceNode(element)) {
            // add this criteria node to the parent and make it the parent itself for rest of the information.
            return new MappingCriteriaNode(getCriteria(element), isDefaultOnChoiceNode(element));
        }
        throw new MappingException(QueryPlugin.Util.getString("MappingLoader.invalid_criteria_node")); //$NON-NLS-1$
    }
    
    /**
     * Process a mapping node that has been encountered.
     * @param element XML Document Element which is the source of the
     * MappingNode
     * @param parentNode MappingNode parent - will be null only the first
     * time, for the root Element
     * @return MappingNode just processed
     */
    MappingBaseNode processMappingNode(Element element, MappingBaseNode parentNode) throws MappingException {
        
        boolean isRootNode = (parentNode instanceof MappingDocument);
        
        // Parse the node based on the node type.
        String nodeType = getElementValue( element, MappingNodeConstants.Tags.NODE_TYPE );
        if (nodeType == null || nodeType.length() == 0) {
            nodeType = MappingNodeConstants.ELEMENT;
        }
         
        // Load the document properties
        if (isRootNode && nodeType.equalsIgnoreCase(MappingNodeConstants.ELEMENT)) {
            MappingDocument doc = (MappingDocument)parentNode;
            loadDocumentProperties(doc, element);
        }
        
        // prior to 5.5, a criteria node also behaved like element, however it has been changed to
        // have only criteria information. however we still need to support the old vdbs with 
        // old type of mapping document (this can be removed after couple versions).
        if (nodeType.equalsIgnoreCase(MappingNodeConstants.ELEMENT)
                        && (getCriteria(element) != null || isDefaultOnChoiceNode(element))) {
            // add this criteria node to the parent and make it the parent itself for rest of the information.
            MappingCriteriaNode node = new MappingCriteriaNode(getCriteria(element), isDefaultOnChoiceNode(element));
            parentNode.addCriteriaNode(node);
            parentNode = node;
        }
        
        if (nodeType.equalsIgnoreCase(MappingNodeConstants.ELEMENT)) {
            MappingElement child = loadElementNode(element, parentNode, isRootNode);
            parentNode.addChildElement(child);
            return child;
        }      
        else if (nodeType.equalsIgnoreCase(MappingNodeConstants.ATTRIBUTE)) {
            loadAttributeNode(element, (MappingElement)parentNode);
        }
        else if (nodeType.equalsIgnoreCase(MappingNodeConstants.CHOICE)) {
            MappingChoiceNode child = loadChoiceNode(element, parentNode);
            parentNode.addChoiceNode(child);
            return child;
        }
        else if (nodeType.equalsIgnoreCase(MappingNodeConstants.CRITERIA)) {
            MappingCriteriaNode child = loadCriteriaNode(element, parentNode);
            parentNode.addCriteriaNode(child);
            return child;
        }                
        else if (nodeType.equalsIgnoreCase(MappingNodeConstants.ALL)) {
            MappingAllNode child = loadAllNode(element, parentNode);
            parentNode.addAllNode(child);
            return child;
        }        
        else if (nodeType.equalsIgnoreCase(MappingNodeConstants.SEQUENCE)) {
            MappingSequenceNode child = loadSequenceNode(element, parentNode);
            parentNode.addSequenceNode(child);
            return child;
        }
        else if (nodeType.equalsIgnoreCase(MappingNodeConstants.COMMENT)) {
            loadCommentNode(element, (MappingElement)parentNode);
        }
        else {
            // should we ignore I am not sure??
            throw new MappingException(QueryPlugin.Util.getString("MappingLoader.unknown_node_type", nodeType)); //$NON-NLS-1$            
        }
        return null;
    }
    
    /**
     * Return Properties holding namespaces declarations, or null if none
     */
    private Namespace[] getNamespaceDeclarations(Element element) {
        ArrayList namespaces = new ArrayList();
        Iterator elements = getChildren(element, MappingNodeConstants.Tags.NAMESPACE_DECLARATION).iterator();
        while (elements.hasNext()){
            Element namespace = (Element)elements.next();
            Element prefixEl = getElement(namespace, MappingNodeConstants.Tags.NAMESPACE_DECLARATION_PREFIX);
            Element uriEl = getElement(namespace, MappingNodeConstants.Tags.NAMESPACE_DECLARATION_URI);
            String prefix = (prefixEl != null ? getTextTrim(prefixEl) : MappingNodeConstants.DEFAULT_NAMESPACE_PREFIX);
            String uri = getTextTrim(uriEl);
            namespaces.add(new Namespace(prefix, uri));
        }
        return (Namespace[])namespaces.toArray(new Namespace[namespaces.size()]);
    }
    
    static String getTextTrim(Element element) {
    	if (element == null) {
    		return null;
    	}
    	String result = element.getTextContent();
    	if (result != null) {
    		return result.trim();
    	}
    	return result;
    }

    /**
     * Get a specific child element of an element.
     */
    Element getElement( Element element, String childName ) {
        return getChild(element, childName);
    }

    String getElementValue( Element element, String childName ) {
        Element child = getChild(element, childName);
        return getTextTrim(child);
    }
    
    String getElementValue( Element element, String childName , String defalt) {
        Element child = getChild(element, childName);
        String result = getTextTrim(child);
        if (result == null) {
        	return defalt;
        }
        return result;
    }    
    
    int getIntElementValue( Element element, String childName , int defalt) {
        Element child = getChild(element, childName);
        if (child != null) {
            return Integer.valueOf(getTextTrim(child)).intValue();
        }
        return defalt;
    } 
    
    boolean getBooleanElementValue( Element element, String childName , boolean defalt) {
        Element child = getChild(element, childName);
        if (child != null) {
            return Boolean.valueOf(getTextTrim(child)).booleanValue();
        }
        return defalt;
    } 
    
    String getName(Element element) {
        return getElementValue(element, MappingNodeConstants.Tags.NAME);
    }

    int getMinOccurrences(Element element) {
        return getIntElementValue(element, MappingNodeConstants.Tags.CARDINALITY_MIN_BOUND, MappingNodeConstants.Defaults.DEFAULT_CARDINALITY_MINIMUM_BOUND.intValue());        
    }
    
    int getMaxOccurrences(Element element) {
        String maxBound = getElementValue(element, MappingNodeConstants.Tags.CARDINALITY_MAX_BOUND );        
        if (maxBound != null && maxBound.equals(MappingNodeConstants.CARDINALITY_UNBOUNDED_STRING)){
            return MappingNodeConstants.CARDINALITY_UNBOUNDED.intValue();
        } else if (maxBound != null){
            return Integer.valueOf(maxBound).intValue();
        }
        else {
            return MappingNodeConstants.Defaults.DEFAULT_CARDINALITY_MAXIMUM_BOUND.intValue();
        }
    }

    String getNameInSource(Element element) {
        return getElementValue(element, MappingNodeConstants.Tags.ELEMENT_NAME);        
    }
    
    String getCommentText(Element element) {
        return getElementValue(element, MappingNodeConstants.Tags.COMMENT_TEXT);        
    }
    
    boolean isOptional(Element element) {
        return getBooleanElementValue( element, MappingNodeConstants.Tags.IS_OPTIONAL, MappingNodeConstants.Defaults.DEFAULT_IS_OPTIONAL.booleanValue());        
    }
    
    String getSource(Element element) {
        return getElementValue(element, MappingNodeConstants.Tags.RESULT_SET_NAME );        
    }
    
    String getCriteria(Element element) {
        return getElementValue(element, MappingNodeConstants.Tags.CRITERIA );        
    }

    String getDefaultValue(Element element) { 
        return getElementValue(element, MappingNodeConstants.Tags.DEFAULT_VALUE);
    }
    
    String getFixedValue(Element element) { 
        return getElementValue(element, MappingNodeConstants.Tags.FIXED_VALUE);
    }
    
    boolean isNillable(Element element) { 
        return getBooleanElementValue(element, MappingNodeConstants.Tags.IS_NILLABLE, MappingNodeConstants.Defaults.DEFAULT_IS_NILLABLE.booleanValue());
    }

    boolean isExcluded(Element element) { 
        return getBooleanElementValue(element, MappingNodeConstants.Tags.IS_EXCLUDED, MappingNodeConstants.Defaults.DEFAULT_IS_EXCLUDED.booleanValue());
    }
    
    boolean isDefaultOnChoiceNode(Element element) { 
        return getBooleanElementValue(element, MappingNodeConstants.Tags.IS_DEFAULT_CHOICE, MappingNodeConstants.Defaults.DEFAULT_IS_DEFAULT_CHOICE.booleanValue());
    }
    
    boolean exceptionOnDefault(Element element) { 
        return getBooleanElementValue(element, MappingNodeConstants.Tags.EXCEPTION_ON_DEFAULT, MappingNodeConstants.Defaults.DEFAULT_EXCEPTION_ON_DEFAULT.booleanValue());
    }

    String getDocumentEncoding(Element element) { 
        return getElementValue(element, MappingNodeConstants.Tags.DOCUMENT_ENCODING, MappingNodeConstants.Defaults.DEFAULT_DOCUMENT_ENCODING);
    }
    
    boolean isFormattedDocument(Element element) { 
        return getBooleanElementValue(element, MappingNodeConstants.Tags.FORMATTED_DOCUMENT, MappingNodeConstants.Defaults.DEFAULT_FORMATTED_DOCUMENT.booleanValue());
    }
    
    boolean isRecursive(Element element) { 
        return getBooleanElementValue(element, MappingNodeConstants.Tags.IS_RECURSIVE, MappingNodeConstants.Defaults.DEFAULT_IS_RECURSIVE.booleanValue());
    }
    
    String getRecursionCriteria(Element element) { 
        return getElementValue(element, MappingNodeConstants.Tags.RECURSION_CRITERIA);
    }
    
    String getRecursionMappingClass(Element element) { 
        return getElementValue(element, MappingNodeConstants.Tags.RECURSION_ROOT_MAPPING_CLASS);
    }
        
    int getRecursionLimit(Element element) {
        return getIntElementValue(element, MappingNodeConstants.Tags.RECURSION_LIMIT, MappingNodeConstants.Defaults.DEFAULT_RECURSION_LIMIT.intValue());        
    }
    
    boolean throwExceptionOnRecursionLimit(Element element) { 
        return getBooleanElementValue(element, MappingNodeConstants.Tags.RECURSION_LIMIT_EXCEPTION, MappingNodeConstants.Defaults.DEFAULT_EXCEPTION_ON_RECURSION_LIMIT.booleanValue());
    }

    List getStagingTableNames(Element element) { 
        ArrayList cacheGroups = new ArrayList();
        Collection tempGroupElements = getChildren(element, MappingNodeConstants.Tags.TEMP_GROUP_NAME);
        for (Iterator i = tempGroupElements.iterator(); i.hasNext();) {
            Element tempGroup = (Element)i.next();
            cacheGroups.add(getTextTrim(tempGroup));            
        }
        return cacheGroups;
    }
    
    String getNormalizeText(Element element) { 
        return getElementValue(element, MappingNodeConstants.Tags.NORMALIZE_TEXT, MappingNodeConstants.Defaults.DEFAULT_NORMALIZE_TEXT);
    }
    
    String getBuitInType(Element element) { 
        return getElementValue(element, MappingNodeConstants.Tags.BUILT_IN_TYPE);
    }
    
    boolean includeAlways(Element element) { 
        return getBooleanElementValue(element, MappingNodeConstants.Tags.ALWAYS_INCLUDE, false);
    }    

    Namespace getNamespace(Element element, Namespace[] localNamespaces, MappingBaseNode parentNode) { 
        String prefix = getElementValue(element, MappingNodeConstants.Tags.NAMESPACE_PREFIX);
        
        if (prefix != null) {
            if (localNamespaces != null) {
                // first try to find the name space in its own namespace attributes
                for (int i = 0; i < localNamespaces.length; i++) {
                    if (localNamespaces[i].getPrefix().equals(prefix)) {
                        return localNamespaces[i];
                    }
                }
            }
            
            // then look in the parent nodes.
            while (parentNode != null) {
                if (parentNode instanceof MappingElement) {
                    MappingElement parentElement = (MappingElement)parentNode;
                    return getNamespace(element, parentElement.getNamespaces(), parentElement.getParentNode());
                }
                parentNode = parentNode.getParentNode();
            }
         
            // default; we should never get here.. except otherwise a namespace is used before its
            // declaration; which is case sometimes.
            Namespace unresolved = new Namespace(prefix);
            this.unresolvedNamespaces.put(prefix, unresolved);
            return unresolved;
        }
        return MappingNodeConstants.NO_NAMESPACE;
    }
    
} // END CLASS
