/*
 * 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 org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.lang.reflect.Type;
import java.lang.reflect.Field;

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

/**
 * @author Dave McCann, Guy Pelletier
 * @since TopLink 10.1.3/EJB 3.0 Preview
 */
public class XMLAccessor extends MetadataAccessor {
    private Node m_node;
    private String m_name;
    private XMLHelper m_helper;
    private Class m_relationType;
    private XPathEngine m_xPathEngine;
    
    /**
     * INTERNAL:
     */
    private XMLAccessor(EntityMappingsXMLProcessor processor, XMLDescriptorMetadata xmlDmd, Node node) {
        setNeedsProcessing(true);
        
        m_node = node;
        m_metadataProcessor = processor;
        m_metadataDescriptor = xmlDmd;
        m_helper = ((EntityMappingsXMLProcessor) m_metadataProcessor).getHelper();
        m_isRelationship = null;    // Signifies that we don't know yet.
        m_xPathEngine = XPathEngine.getInstance();
    }
    
    /**
     * INTERNAL:
     */
    public XMLAccessor(String attributeName, EntityMappingsXMLProcessor processor, XMLDescriptorMetadata dmd, Node node) {
        this(processor, dmd, node);
        m_attributeName = attributeName;
    }
    
    /**
     * INTERNAL:
     */
    public String getAnnotatedElement() {
    	return getAttributeName();
    }
    
    /**
     * INTERNAL:
     * Return the attribute name for this accessor.  If not already set, will 
     * attempt to get the @name attribute of m_node, otherwise, "" is returned. 
     */
    public String getAttributeName() {
        if (m_attributeName == null) {
        	Node nameNode = m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.ATT_NAME});
        	if (nameNode != null && nameNode.getNodeValue() != null) {
        		m_attributeName = nameNode.getNodeValue();
        	} else {
        		m_attributeName = "";
        	}
        }
        
    	return m_attributeName;
    }
    
    /**
     * INTERNAL:
     */
    public Node getAttributeNode() {
    	return m_node;
    }
    
    /**
     * INTERNAL:
     */
    public NodeList getAttributeOverrideNodes() {
    	return m_xPathEngine.selectNodes(m_node, new String[] {XMLConstants.ATTRIBUTE_OVERRIDE});
    }

    /**
     * INTERNAL:
     */
    public NodeList getCascade() {
    	return m_xPathEngine.selectNodes(m_node, new String[] {XMLConstants.CASCADE, XMLConstants.ALL_CHILDREN});
    }
    
    /**
     * INTERNAL:
     */
    public Node getColumnNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.COLUMN}); 
    }
    
    /**
     * INTERNAL:
     */
    public Node getEnumeratedNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.ENUMERATED, XMLConstants.TEXT});
    }
    
    /**
     * INTERNAL:
     * Get the fetch attribute value for this node. This method will always 
     * return a value, even if the node does not have a fetch attribute - it 
     * should only be called for applicable mappings: relationship and basic.
     */
    public String getFetchType() {
    	String fetchVal;
        
    	if (isManyToOne() || isOneToOne() || isBasic()) {
    		fetchVal = XMLConstants.EAGER;
    	} else {
    		fetchVal = XMLConstants.LAZY;
    	}
        
    	Node fetchNode = m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.ATT_FETCH});
    	if (fetchNode != null) {
    		fetchVal = fetchNode.getNodeValue();
    	}
    	
    	return fetchVal;
    }
    
    /**
     * INTERNAL:
     */
    public Node getFetchNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.ATT_FETCH});
    }
    
    /**
     * INTERNAL:
     */
    public Node getGeneratedValueNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.GENERATED_VALUE});
    }
    
    /**
     * INTERNAL:
     */
    public Class getJavaClass() {
    	return getMetadataDescriptor().getJavaClass();
    }

    /**
     * INTERNAL:
     */
    public Node getJoinTableNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.JOIN_TABLE});
    }

    /**
     * INTERNAL:
     */
    public Node getLobNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.LOB, XMLConstants.TEXT});
    }

    /**
     * INTERNAL:
     */
    public Node getMapKeyNode() {
        return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.MAPKEY});
    }
    
    /**
     * INTERNAL:
     * Assumes a mapped-by attribute exists - should call 'hasMappedBy' before 
     * calling this method.
     * 
     * @return empty string if no mapped-by, otherwise the value of the 
     * mapped-by attribute
     */
    public String getMappedBy() {
    	if (!hasMappedBy()) {
    		return "";
    	}
        
    	return getMappedByNode().getNodeValue();
    }
    
    /**
     * INTERNAL:
     */
    public Node getMappedByNode() {
        return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.ATT_MAPPED_BY});
    }
    
    /**
     * INTERNAL:
     * Return the XMLDescriptorMetadata for this accessor.
     */
    public XMLDescriptorMetadata getMetadataDescriptor() {
        return (XMLDescriptorMetadata) m_metadataDescriptor;
    }
    
    /**
     * INTERNAL:
     */
    public String getMapKey() {
    	Node mapKeyNode = getMapKeyNode();
    	String mapKey = "";
        
    	if (mapKeyNode.getNodeValue() != null) {
    		mapKey = mapKeyNode.getNodeValue();
    	}
        
        return mapKey;
    }

    /**
     * INTERNAL:
     * Return the attribute (field) name if FIELD access-type, or the get method 
     * name if PROPERTY.
     */
    public String getName() {
    	if (m_name == null) {
	    	if (getMetadataDescriptor().usesPropertyAccess()) {
	    		String[] methodNames = MetadataHelper.getMethodNamesForField(getAttributeName(), getRelationType());
	    		m_name = methodNames[0];
	    		getMetadataDescriptor().addSetMethodName(m_name, methodNames[1]);
	    	} else {
	    		m_name = getAttributeName();
	    	}
    	}
    	
        return m_name;
    }
    
    /**
     * INTERNAL:
     */
    public Node getOrderByNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.ORDER_BY, XMLConstants.TEXT});
    }
    
    /**
     * INTERNAL:
     * Return the raw class for this accessor.  Will be the same as the relation
     * type assuming JDK 1.5 generics aren't being used in XML processing.
     */
    public Class getRawClass() {
        return getRelationType();
    }
   
    /**
     * INTERNAL:
     * Return the reference class for this accessor.
     */
    public Class getReferenceClass() {
    	if (m_referenceClass == null) {
    		m_referenceClass = getRelationType();
    	}
        
        return m_referenceClass;
    }
    
    /**
     * INTERNAL:
     * Return the relation type of this accessor.
     */
    public Class getRelationType() {
        if (m_relationType == null) {
        	m_relationType = MetadataHelper.getClassForAttribute(m_attributeName, getMetadataDescriptor());
        }
        
        return m_relationType;
    }
    
    /**
     * INTERNAL:
     */
    public Node getSequenceGeneratorNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.SEQUENCE_GENERATOR});
    }

    /**
     * INTERNAL:
     */
    public Node getTableGeneratorNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.TABLE_GENERATOR});
    }

    /**
     * INTERNAL:
     * Return the target entity class for the relationship mapping represented 
     * by this accessor.  Will return the class name based on the @target-entity
     * attribute if defined.  If this is not defined, and the attribute's 
     * reference class is not a collection, the reference class will be 
     * returned.  If the reference class is a collection, the platform is 
     * JDK 1.5, and the collection is defined via generics, the generic type 
     * will be returned.  Otherwise, void.class is returned.
     */
    public Class getTargetEntity() {
    	Node tgtNode;
    	if ((tgtNode = getTargetEntityNode()) != null) {
    		return m_helper.getClassForName(tgtNode.getNodeValue());
    	}
        
		// TODO: remove JDK 1.5 dependancy
		// if no target-entity attribute, check reference class
		if (MetadataHelper.isSupportedCollectionClass(getReferenceClass())) {
			// need to check for generics here if platform is JDK 1.5
			Field fld = MetadataHelper.getFieldForAttribute(getAttributeName(), getJavaClass());
		
        	if (fld != null) {
    			Type fldType = MetadataHelper.getGenericType(fld);
    	
        		if (MetadataHelper.isGenericCollectionType(fldType)) {
    				return MetadataHelper.getReturnTypeFromGeneric(fldType);
    			}
			}
		
        	return void.class;
		}
		
        // if not a collection, then the target is the reference class, i.e. Employee
		return getReferenceClass();
    }

    /**
     * INTERNAL:
     */
    public Node getTargetEntityNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.ATT_TARGET_ENTITY});
    }
    
    /**
     * INTERNAL:
     */
    public Node getTemporalNode() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.TEMPORAL, XMLConstants.TEXT});
    }

    /**
     * INTERNAL:
     */
    public boolean hasAttributeOverrides() {
    	return getAttributeOverrideNodes() != null;
    }
    
    /**
     * INTERNAL:
	 * Method to check if m_node has one or more cascade sub-elements.
     */
	public boolean hasCascade() {
    	return (getCascade() != null && getCascade().getLength() > 0);
    }

	/**
     * INTERNAL:
	 * Method to check if m_node has a column sub-element.
     */
	public boolean hasColumn() {
    	return getColumnNode() != null;
    }
    
    /**
     * INTERNAL:
	 * Method to check if m_node has an enumerated sub-element.
     */
	public boolean hasEnumerated() {
    	return getEnumeratedNode() != null;
    }

    /**
     * INTERNAL:
	 * Method to check if m_node has a fetch attribute.
     */
	public boolean hasFetch() {
    	return getFetchNode() != null;
    }
	
	/**
     * INTERNAL:
	 * Method to check if m_node has a single join-column sub-element.
     */
	public boolean hasJoinColumn() {
    	NodeList joinColumnNodes = m_xPathEngine.selectNodes(m_node, new String[] {XMLConstants.JOIN_COLUMN});
    	return (joinColumnNodes != null && joinColumnNodes.getLength() == 1);
    }
    
    /**
     * INTERNAL:
	 * Method to check if an annotated element has more than one join-column 
     * sub-elements.
     */
	public boolean hasJoinColumns() {
    	NodeList joinColumnNodes = m_xPathEngine.selectNodes(m_node, new String[] {XMLConstants.JOIN_COLUMN});
    	return (joinColumnNodes != null && joinColumnNodes.getLength() > 1);
    }

    /**
     * INTERNAL:
	 * Method to check if m_node has a map-key sub-element.
     */
	public boolean hasMapKey() {
		return getMapKeyNode() != null;
	}
	
    /**
     * INTERNAL:
	 * Method to check if m_node has a mapped-by attribute.
     */
	public boolean hasMappedBy() {
		return getMappedByNode() != null;
	}

	/**
     * INTERNAL:
	 * Method to check if m_node has an order-by sub-element.
     */
	public boolean hasOrderBy() {
		Node orderByNode = getOrderByNode();
		return (orderByNode != null && orderByNode.getNodeValue() != null);
	}
	
    /**
     * INTERNAL:
	 * Method to check if m_node has a single primary-key-join-column 
     * sub-element.
     */
	public boolean hasPrimaryKeyJoinColumn() {
    	NodeList joinColumnNodes = m_xPathEngine.selectNodes(m_node, new String[] {XMLConstants.PK_JOIN_COLUMN});
    	return (joinColumnNodes != null && joinColumnNodes.getLength() == 1);
    }
    
    /**
     * INTERNAL:
	 * Method to check if m_node has more than one primary-key-join-column 
     * sub-element.
     */
	public boolean hasPrimaryKeyJoinColumns() {
    	NodeList joinColumnNodes = m_xPathEngine.selectNodes(m_node, new String[] {XMLConstants.PK_JOIN_COLUMN});
    	return (joinColumnNodes != null && joinColumnNodes.getLength() > 1);
    }
    
    /**
     * INTERNAL:
	 * Method to check if m_node has a sequence-generator sub-element.
     */
	public boolean hasSequenceGenerator() {
    	return getSequenceGeneratorNode() != null;
    }
    
    /**
     * INTERNAL:
	 * Method to check if m_node has a sql-result-set-mapping sub-element.
     */
	public boolean hasSqlResultSetMapping() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.SQL_RESULT_SET_MAPPING}) != null;
    }

    /**
     * INTERNAL:
	 * Method to check if m_node has a table-generator sub-element.
     */
	public boolean hasTableGenerator() {
    	return getTableGeneratorNode() != null;
    }
    
    /**
     * INTERNAL:
	 * Method to check if m_node has a temporal sub-element.
     */
	public boolean hasTemporal() {
    	return getTemporalNode() != null;
    }
    
	/**
     * INTERNAL:
     * Return true if this accessor represents a basic mapping.
     */
	public boolean isBasic() {
		return m_node.getLocalName().equals(XMLConstants.BASIC);
    }
	
	/**
     * INTERNAL:
     * Return true if this accessor represents an aggregate mapping. True is
     * returned if an embedded sub-element exists, or if the reference class
     * is tagged with an embeddable local name.
     */
	public boolean isEmbedded() {
		Node embeddedNode = m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.EMBEDDED});
		Node embeddedIdNode = m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.EMBEDDED_ID});
		Node referenceClassNode =  m_helper.locateNode(getReferenceClass(), XMLConstants.EMBEDDABLE);
		
		return embeddedNode != null || (referenceClassNode != null && embeddedIdNode == null);
    }
    
    /**
     * INTERNAL:
     * Return true if this accessor represents an aggregate id mapping, i.e. has
     * an embedded-id sub-element.
     */
	public boolean isEmbeddedId() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.EMBEDDED_ID}) != null;
    }
     
	/**
     * INTERNAL:
	 * Method to check if m_node represents a primary key.
	 */
	public boolean isId() {
		return m_node.getLocalName().equals(XMLConstants.ID);
	}

    /**
     * INTERNAL:
     * Return true if this accessor represents an BLOB/CLOB mapping, i.e. has a 
     * lob sub-element.
     */
	public boolean isLob() {
    	return getLobNode() != null;
    }
    
    /**
     * INTERNAL:
     * Return true if this accessor represents a m-m relationship.
     */
	public boolean isManyToMany() {
		if (m_node.getLocalName().equals(XMLConstants.MANY_TO_MANY)) {
            if (MetadataHelper.isSupportedCollectionClass(getRawClass())) {
                return true;
            }
            
            throw ValidationException.invalidCollectionTypeForRelationship(getRawClass(), m_attributeName);
		}   
        
        return false;
    }
    
    /**
     * INTERNAL:
	 * Return true if this accessor represents a m-1 relationship.
     */
	public boolean isManyToOne() {
		return m_node.getLocalName().equals(XMLConstants.MANY_TO_ONE);
    }
    
    /**
     * INTERNAL:
	 * Return true if this accessor represents a 1-m relationship.
     */
	public boolean isOneToMany() {
		if (m_node.getLocalName().equals(XMLConstants.ONE_TO_MANY)) {
            if (MetadataHelper.isSupportedCollectionClass(getRawClass())) {
                return true;
            } else {
                throw ValidationException.invalidCollectionTypeForRelationship(getRawClass(), m_attributeName);
            }
        }
        
        return false;
    }
    
	/**
     * INTERNAL:
     * Return true if this accessor represents a 1-1 relationship.
     */
	public boolean isOneToOne() {
		return m_node.getLocalName().equals(XMLConstants.ONE_TO_ONE);
    }
    
	/**
	 * INTERNAL:
	 * Indicates if the mapping associated with the node is optional. The 
     * default is true for all applicable types (basic, manyToOne, oneToOne).
	 */
	public boolean isOptional() {
        boolean optional = true;
		Node optionalNode = m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.ATT_OPTIONAL}); 
        
        if (optionalNode != null) {
            optional = Boolean.getBoolean(optionalNode.getNodeValue());
        }
        
        return optional;
	}
	
    /**
     * INTERNAL:
     */
    public boolean isTargetEntityDefinedInDocument() {
    	return m_helper.locateNode(getTargetEntity()) != null;
    }
    
    /**
     * INTERNAL:
	 * Return true if this accessor represents an optimistic locking field.
     */
	public boolean isVersion() {
    	return m_xPathEngine.selectSingleNode(m_node, new String[] {XMLConstants.VERSION}) != null;
    }
 
    /**
     * INTERNAL:
	 * Set the reference class for this accessor.
     */
    public void setReferenceClass(Class potentialReferenceClass, String context) {
        m_referenceClass = MetadataHelper.getReferenceClass(getRelationType(), potentialReferenceClass);
    	
        // If the reference class and attribute type (relationType) are equal, 
        // log a message
        if (m_referenceClass == getRelationType()) {
        	m_metadataProcessor.getLogger().logConfigMessage(context, m_attributeName, m_referenceClass);
        }
	}
    
    /**
     * INTERNAL:
	 * Store ourself on our respective MetadataDescriptor.
     */
    public void store() {
        getMetadataDescriptor().addAccessor(this);
	}
    
    /**
     * INTERNAL:
	 * Store ourself on our respective MetadataDescriptor.
     */
    public boolean usesIndirection() {
        return getFetchType().equals(XMLConstants.LAZY);
    }
}
