/*
 * The contents of this file are subject to the terms 
 * of the Common Development and Distribution License 
 * (the "License").  You may not use this file except 
 * in compliance with the License.
 * 
 * You can obtain a copy of the license at 
 * glassfish/bootstrap/legal/CDDLv1.0.txt or 
 * https://glassfish.dev.java.net/public/CDDLv1.0.html. 
 * See the License for the specific language governing 
 * permissions and limitations under the License.
 * 
 * When distributing Covered Code, include this CDDL 
 * HEADER in each file and include the License file at 
 * glassfish/bootstrap/legal/CDDLv1.0.txt.  If applicable, 
 * add the following below this CDDL HEADER, with the 
 * fields enclosed by brackets "[]" replaced with your 
 * own identifying information: Portions Copyright [yyyy] 
 * [name of copyright owner]
 */
// Copyright (c) 1998, 2006, Oracle. All rights reserved.  
package oracle.toplink.essentials.internal.ejb.cmp3.xml;

import java.io.IOException;
import java.net.URL;
import java.util.GregorianCalendar;

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

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import oracle.toplink.essentials.exceptions.ValidationException;
import oracle.toplink.essentials.exceptions.XMLParseException;
import oracle.toplink.essentials.internal.ejb.cmp3.metadata.MetadataHelper;
import oracle.toplink.essentials.internal.ejb.cmp3.metadata.MetadataLogger;


/**
 * Utility class used for handling element inspection.
 */
public class XMLHelper {
	private Document m_xmlDoc;
	private ClassLoader m_loader;
	private String m_defaultPkg;
	private XPathEngine m_xPathEngine;
	
	/**
	 * 
	 * @param xmlDoc
	 * @param loader
	 */
	public XMLHelper(Document xmlDoc, ClassLoader loader) {
		m_xPathEngine = XPathEngine.getInstance();
		m_loader = loader;
		m_xmlDoc = xmlDoc;
		
		// process package
		Node node = m_xPathEngine.selectSingleNode(m_xmlDoc, new String[] {XMLConstants.ENTITY_MAPPINGS, XMLConstants.PACKAGE, XMLConstants.TEXT});
        if (node != null && node.getNodeValue() != null) {
        	m_defaultPkg = node.getNodeValue();
        } else {
        	m_defaultPkg = "";
        }
	}

	 
	/**
	 * 
	 * @param xmlDocURL
	 * @param loader
	 */
	public XMLHelper(URL xmlDocURL, ClassLoader loader) {
		this(parseDocument(xmlDocURL, loader), loader);
	}

    /**
     * INTERNAL:
     * Helper method to indicate if an entity has callback methods defined on it - i.e. is a listener
     * 
     */
    boolean entityHasCallbackMethod(Node entityNode) {
        return ((m_xPathEngine.selectSingleNode(entityNode, new String[] {XMLConstants.PRE_PERSIST})  != null) ||
                (m_xPathEngine.selectSingleNode(entityNode, new String[] {XMLConstants.POST_PERSIST}) != null) ||
                (m_xPathEngine.selectSingleNode(entityNode, new String[] {XMLConstants.PRE_REMOVE})   != null) ||
                (m_xPathEngine.selectSingleNode(entityNode, new String[] {XMLConstants.POST_REMOVE})  != null) ||
                (m_xPathEngine.selectSingleNode(entityNode, new String[] {XMLConstants.PRE_UPDATE})   != null) ||
                (m_xPathEngine.selectSingleNode(entityNode, new String[] {XMLConstants.POST_UPDATE})  != null) ||
                (m_xPathEngine.selectSingleNode(entityNode, new String[] {XMLConstants.POST_LOAD})    != null));
    }

    /**
     * 
     * @param className
     * @return
     */
	Class getClassForName(String className) {
		return MetadataHelper.getClassForName(getFullyQualifiedClassName(className), m_loader);
	}

    /**
	 * INTERNAL:
	 * Return the Class for a given node.  This method assumes that the node requires
	 * a class attribute, i.e. entity or mapped-superclass.
	 * 
	 * @param node
	 * @return
	 */
	Class getClassForNode(Node node) {
		return MetadataHelper.getClassForName(getClassNameForNode(node), m_loader);
	}
	
	/**
	 * INTERNAL:
	 * Return the fully qualified class name for a given node.  This method assumes that 
	 * the node requires a class attribute, i.e. entity or mapped-superclass.
	 * 
	 * @param node
	 * @return
	 */
	String getClassNameForNode(Node node) {
        // process @class (required)
        return getFullyQualifiedClassName(m_xPathEngine.selectSingleNode(node, new String[] {XMLConstants.ATT_CLASS}).getNodeValue());
	}
    
	/**
	 * INTERNAL:
	 * Return the instance document associated with this helper.
	 * 
	 * @return
	 */
	Document getDocument() {
		return m_xmlDoc;
	}

    /**
     * INTERNAL:
     * This convenience method will attempt to fully qualify a class name if required.  This assumes
     * that the className value is non-null, and a "qualified" class name contains at least one '.'
     * 
     * @param className 
     * @return className if there is no default package value or className contains one or more '.',
     * otherwise the default package is prepended to className and that new value is returned
     */
	String getFullyQualifiedClassName(String className) {
        return getFullyQualifiedClassName(className, m_defaultPkg);
    }
    
    /**
     * INTERNAL:
     * This convenience method will attempt to fully qualify a class name if required.  This assumes
     * that the className value is non-null, and a "qualified" class name contains at least one '.'
     * 
     * @param className 
     * @param packageName default package name to use if the class name is not fully qualified
     * @return className if there is no default package value or className contains one or more '.',
     * otherwise the default package is prepended to className and that new value is returned
     */
    static String getFullyQualifiedClassName(String className, String packageName) {
        // if there is no global package defined or the class name is qualified, return className
        if (packageName.equals("") || className.indexOf(".") != -1) {
            return className;
        }
        
        // prepend the package to the class name
        // format of global package is "foo.bar."
        if (packageName.endsWith(".")) {
            return (packageName + className);
        }
        // format of global package is "foo.bar"
        return (packageName + "." + className);
    }
	
    /**
     * INTERNAL:
     * This convenience method determines the type of relationship mapping the
     * node represents, and returns the appropriate logging context.
     * 
     * @param mappingNode 
     * @return 
     */
    String getLoggingContextForDefaultMappingReferenceClass(Node mappingNode) {
        if (mappingNode.getLocalName().equals(XMLConstants.ONE_TO_ONE)) {
            return MetadataLogger.ONE_TO_ONE_MAPPING_REFERENCE_CLASS;
        }
        if (mappingNode.getLocalName().equals(XMLConstants.ONE_TO_MANY)) {
            return MetadataLogger.ONE_TO_MANY_MAPPING_REFERENCE_CLASS;
        }
        if (mappingNode.getLocalName().equals(XMLConstants.MANY_TO_ONE)) {
            return MetadataLogger.MANY_TO_ONE_MAPPING_REFERENCE_CLASS;
        }
        // assume many-to-many
        return MetadataLogger.MANY_TO_MANY_MAPPING_REFERENCE_CLASS;
    }
    
    /**
     * INTERNAL:
	 * Checks if an entity inherits from an mapped-superclass. If so, the node
     * for the mapped-superclass is returned.
     * 
     * @param entityClass
     */
	Node getMappedSuperclassNode(Class entityClass) {
        Class parent = entityClass.getSuperclass();
        while (parent != Object.class) {
            // check any mapped-superclass for class = parent
            Node mappedClassNode = locateNodeForMappedSuperclass(parent);
            if (mappedClassNode != null) {
                return mappedClassNode;
            }
            parent = parent.getSuperclass();
        }
        return null;
    }

	/**
	 * INTERNAL:
	 * @return
	 */
	String getPackage() {
		return m_defaultPkg;
	}
	
    /**
     * INTERNAL:
     * Check if a DirectToFieldMapping needs to have a serialized object
     * converter.  If the class is a primitive, primitive wrapper, or a date,
     * calendar or timstamp a converter is not required.
     */
    boolean isSerializedObjectConverterRequired(Class cls) {
        // if the cls is primitive, return false
        if (cls.equals(int.class) ||
            cls.equals(long.class) ||
            cls.equals(char.class) ||
            cls.equals(char[].class) ||
            cls.equals(byte.class) ||
            cls.equals(byte[].class) ||
            cls.equals(short.class) ||
            cls.equals(float.class) ||
            cls.equals(double.class) ||
            cls.equals(boolean.class)) {
            return false;
        }
        
        // if the cls is a primitive wrapper, return false
        if (cls.equals(Long.class) ||
            cls.equals(Short.class) ||
            cls.equals(Float.class) ||
            cls.equals(Byte.class) ||
            cls.equals(Byte[].class) ||
            cls.equals(String.class) ||
            cls.equals(Double.class) ||
            cls.equals(Number.class) ||
            cls.equals(Integer.class) ||
            cls.equals(Character.class) ||
            cls.equals(Character[].class) ||
            cls.equals(java.math.BigInteger.class) ||
            cls.equals(java.math.BigDecimal.class)) {    
            return false;
        }   
        
        // if the cls is one of these classes, return false
        if (cls.equals(java.sql.Date.class) ||
            cls.equals(java.util.Date.class) ||
            cls.equals(GregorianCalendar.class) ||
            cls.equals(java.util.Calendar.class) ||
            cls.equals(java.sql.Timestamp.class)) {
            return false;
        }
        
        // a serialized object converted is required
        return true;
    }
    
    /**
     * INTERNAL:
     * Returns the Java class for a given entity.
     * 
     * @param entityName 
     * @return 
     */
    Class locateClassForEntity(String entityName) {
    	XPathEngine xPathEngine = XPathEngine.getInstance();
        Node entityNode = locateNodeForEntity(entityName);
        if (entityNode != null) {
            // process @class (required)
            return MetadataHelper.getClassForName(getFullyQualifiedClassName(xPathEngine.selectSingleNode(entityNode, new String[] {XMLConstants.ATT_CLASS}).getNodeValue()), m_loader);
        }
        return null;
    }
    
    /**
     * INTERNAL:
     * Locate a node in the DOM tree for the given class.  Will look for
     * an entity, embeddable, or mapped-superclass node with @class matching
     * the class name.
     * 
     * @param cls
     * @return
     */
    Node locateNode(Class cls) {
    	Node result = null;
    	// try entity
    	result = locateNodeForEntity(cls);
    	if (result == null) {
    		// try mapped-superclass
        	result = locateNodeForMappedSuperclass(cls);
    	}
    	if (result == null) {
    		// try embeddable
    		result = locateNodeForEmbeddable(cls);
    	}
    	return result;
    }
    
    /**
     * INTERNAL:
     * Locate a node in the DOM tree for a given class.  
     * 
     * @param className the name of the class to search the DOM for
     * @param searchString the node name to search for, i.e. ENTITY or MAPPED_SUPERCLASS
     * @return 
     */
    Node locateNode(Class cls, String searchString) {
        Node node;
        NodeList nodes = m_xPathEngine.selectNodes(m_xmlDoc, new String[] {XMLConstants.ENTITY_MAPPINGS, searchString});

        if (nodes != null) {
            // for each entity in the instance doc...
            for (int i=0; i<nodes.getLength(); i++) {
                node = nodes.item(i);
                // process @class (required)
                if (getClassNameForNode(node).equals(cls.getName())) {
                    return node;
                }
            }
        }
        return null;
    }

    /**
     * INTERNAL:
     * Locate a node in the DOM tree for a given attribute name.  
     * 
     * @param node the node to search for the attribute in
     * @param attributeName the name of the entity to search the DOM for
     * @return 
     */
    Node locateNodeForAttribute(Node node, String attributeName) {
        NodeList attributeNodes = m_xPathEngine.selectNodes(node, new String[] {XMLConstants.ATTRIBUTES, XMLConstants.ALL_CHILDREN});

        if (attributeNodes != null) {
            Node attributeNode;
            // for each entity attribute...
            for (int i=0; i<attributeNodes.getLength(); i++) {
            	attributeNode = attributeNodes.item(i);
                // process @name (required)
                if (m_xPathEngine.selectSingleNode(attributeNode, new String[] {XMLConstants.ATT_NAME}).getNodeValue().equals(attributeName)) {
                    return attributeNode;
                }
            }
        }
        return null;
    }

    /**
     * INTERNAL:
     * 
     * @param owningClass
     * @param attributeName
     * @return
     */
    Node locateNodeForEntityAttribute(Class owningClass, String attributeName) {
        Node node = locateNodeForEntity(owningClass);
        if (node == null) { 
        	return null; 
        }
    	return locateNodeForAttribute(node, attributeName);
    }

    /**
     * INTERNAL:
     * Locate a node in the DOM tree for a given class.  
     * 
     * @param cls the class of the embeddable to search the DOM for
     * @return 
     */
    Node locateNodeForEmbeddable(Class cls) {
        return locateNode(cls, XMLConstants.EMBEDDABLE);
    }

    /**
     * INTERNAL:
     * Locate a node in the DOM tree for a given entity name.  
     * 
     * @param embeddable the name of the embeddable to search the DOM for
     * @return 
     */
    Node locateNodeForEmbeddable(String embeddable) {
        NodeList nodes = m_xPathEngine.selectNodes(m_xmlDoc, new String[] {XMLConstants.ENTITY_MAPPINGS, XMLConstants.EMBEDDABLE});
        if (nodes != null) {
            Node node;
            // for each entity in the instance doc...
            for (int i=0; i<nodes.getLength(); i++) {
                node = nodes.item(i);
                // process @name
                Node attr = m_xPathEngine.selectSingleNode(node, new String[] {XMLConstants.ATT_NAME});
                if (attr != null) {
                    if (attr.getNodeValue().equals(embeddable)) {
                        return node;
                    }
                }
            }
        }
        return null;
    }
    
    /**
     * INTERNAL:
     * Locate a node in the DOM tree for a given class.  
     * 
     * @param cls the class of the entity to search the DOM for
     * @return 
     */
    Node locateNodeForEntity(Class cls) {
        return locateNode(cls, XMLConstants.ENTITY);
    }

    /**
     * INTERNAL:
     * Locate a node in the DOM tree for a given entity name.  
     * 
     * @param entityName the name of the entity to search the DOM for
     * @return 
     */
    Node locateNodeForEntity(String entityName) {
        NodeList nodes = m_xPathEngine.selectNodes(m_xmlDoc, new String[] {XMLConstants.ENTITY_MAPPINGS, XMLConstants.ENTITY});
        if (nodes != null) {
            Node node;
            // for each entity in the instance doc...
            for (int i=0; i<nodes.getLength(); i++) {
                node = nodes.item(i);
                // process @name
                Node attr = m_xPathEngine.selectSingleNode(node, new String[] {XMLConstants.ATT_NAME});
                if (attr != null) {
                    if (attr.getNodeValue().equals(entityName)) {
                        return node;
                    }
                }
            }
        }
        return null;
    }
    
    /**
     * INTERNAL:
     * 
     * @param owningClass
     * @param attributeName
     * @return
     */
    Node locateNodeForMappedSuperclassAttribute(Class owningClass, String attributeName) {
        Node node = locateNodeForMappedSuperclass(owningClass);
        if (node == null) {
        	return null;
        }
    	return locateNodeForAttribute(node, attributeName);
    }
    
   	/**
     * INTERNAL:
     * Locate a node in the DOM tree for a given class.  
     * 
     * @param cls the class of the mapped-superclass to search the DOM for
     * @return 
     */
    Node locateNodeForMappedSuperclass(Class cls) {
        return locateNode(cls, XMLConstants.MAPPED_SUPERCLASS);
    }

    /**
     * INTERNAL:
     * Return the parent entity in an entity class hierarchy
     * 
     * @param entityClass 
     * @return 
     */
    Class locateParentEntity(Class entityClass) {    
        Class superClass = entityClass.getSuperclass();
        if (superClass != null) {
	        while (superClass != Object.class) {
	            if (locateNodeForEntity(superClass) != null) {
	                return superClass;
	            }
	            
	            superClass = superClass.getSuperclass();
	        }
        }
        return entityClass;
    }

    /**
     * INTERNAL:
     * Return the root entity in an entity class hierarchy
     * 
     * @param entityClass 
     * @return 
     */
    Class locateRootEntity(Class entityClass) {    
    	// TODO: we may want to traverse like locateParentEntity()...
    	Class superClass = entityClass.getSuperclass();
        if (superClass != null) {
            // check to make sure it is an entity
            Node entityNode = locateNodeForEntity(superClass);
            if (entityNode != null) {
                // now check for another superclass
                return locateRootEntity(superClass);
            }
        }
        return entityClass;
    }
    
    /**
     * INTERNAL:
     * Indicates if a given node has an inheritance sub-element.
     * 
     * @return true if node has an inheritance sub-element, false if 
     * node is null or no inheritance sub-element exists
     */
    boolean nodeHasInheritance(Node node) {
    	if (node == null) {
    		return false;
    	}
    	return m_xPathEngine.selectSingleNode(node, new String[] {XMLConstants.INHERITANCE}) != null;
    }

    /**
     * INTERNAL:
     * Indicates if a given node has a primary-key-join-column sub-element.
     * 
     * @return true if node has a primary-key-join-column sub-element, false if 
     * node is null or no primary-key-join-column sub-element exists
     */
    boolean nodeHasPrimaryKeyJoinColumns(Node node) {
    	if (node == null) {
    		return false;
    	}
    	NodeList nodes = m_xPathEngine.selectNodes(node, new String[] {XMLConstants.PK_JOIN_COLUMN});
    	return (nodes != null && nodes.getLength() > 0);
    }

    /**
     * INTERNAL:
     * Build a DOM from an instance document using the provided URL.
     * 
     * @param xmlDoumentURL the URL of the instance document to be processed
     */
    static Document parseDocument(URL xmlDocumentURL, ClassLoader loader) {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        dbf.setAttribute(XMLConstants.SCHEMA_LANGUAGE, XMLConstants.XML_SCHEMA);
        dbf.setValidating(true);
        
        // attempt to load the schema from the classpath
        URL schemaURL = loader.getResource(XMLConstants.ORM_SCHEMA_NAME);
        if (schemaURL != null) {
        	dbf.setAttribute(XMLConstants.JAXP_SCHEMA_SOURCE, schemaURL.toString());
        }
        
        // create a document builder
        DocumentBuilder db;
        try {
            db = dbf.newDocumentBuilder();
        } catch (ParserConfigurationException pex) {
            throw XMLParseException.exceptionCreatingDocumentBuilder(xmlDocumentURL.getFile(), pex);
        }
        
        // set the parse exception handler
        XMLExceptionHandler xmlExceptionHandler = new XMLExceptionHandler();
        db.setErrorHandler(xmlExceptionHandler);
        
        // parse the document
        Document doc = null;
        try {
            doc = db.parse(xmlDocumentURL.openStream());
        } catch (IOException ioex) {
            throw XMLParseException.exceptionReadingXMLDocument(xmlDocumentURL.getFile(), ioex);
        } catch (SAXException saxex) {
        	// XMLExceptionHandler will handle parse exceptions
        }
        
        XMLException xmlEx = xmlExceptionHandler.getXMLException();
        if (xmlEx != null) {
        	throw ValidationException.invalidEntityMappingsDocument(xmlDocumentURL.getFile(), xmlEx);
        }
        
        return doc;
    }
}
