/*
 * Copyright 2006 Keith Visco, Ralf Joachim
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.exolab.castor.xml.util;

import org.exolab.castor.mapping.ClassDescriptor;
import org.exolab.castor.mapping.FieldDescriptor;
import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.mapping.MapItem;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.mapping.loader.AbstractFieldDescriptor;
import org.exolab.castor.mapping.loader.FieldHandlerImpl;
import org.exolab.castor.xml.FieldValidator;
import org.exolab.castor.xml.NodeType;
import org.exolab.castor.xml.XMLClassDescriptor;
import org.exolab.castor.xml.XMLFieldDescriptor;
import org.exolab.castor.xml.descriptors.CoreDescriptors;
import org.exolab.castor.xml.handlers.DateFieldHandler;

import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.Properties;

/**
 * XML field descriptor. Wraps {@link FieldDescriptor} and adds
 * XML-related information, type conversion, etc.
 * <p/>
 * Note: When using a GeneralizedFieldHandler the getFieldType() methods of handler and
 *       descriptor need to return the same result.
 * 
 * @author <a href="mailto:keith AT kvisco DOT com">Keith Visco</a>
 * @author <a href="mailto:ralf DOT joachim AT syscon-world DOT de">Ralf Joachim</a>
 * @version $Revision: 6230 $ $Date: 2006-04-13 06:47:36 -0600 (Thu, 13 Apr 2006) $
 */
public class XMLFieldDescriptorImpl
extends AbstractFieldDescriptor
implements XMLFieldDescriptor {
    private static final String WILD_CARD = "*";

    private static final String NULL_CLASS_ERR
        = "The 'type' argument passed to the constructor of "
         + "XMLFieldDescriptorImpl may not be null.";

    private static final String NULL_NAME_ERR
        = "The 'fieldName' argument passed to the constructor of "
         + "XMLFieldDescriptorImpl may not be null.";

    /** The index of this field within the constructor arguments.
     *  Note: This field is only applicable if the field is an attribute field and it's
     *  supposed to be set via the constructor. A value less than zero indicates that
     *  this field is not part of the constructor arguments. */
     private int _argIndex = -1;

    /** True if the field is a container field. */
    private boolean _container = false;
    
    /** Flag to indicate that objects should be added to their as soon as they are
     *  created, but before they are finished being populated. */
    private boolean _incremental = false;

    /** True if the field is a reference to another Object in the hierarchy. */
    public boolean _isReference = false;
    
    private boolean _isWild = false;
    
    /** True if the field type is mapped in a Hashtable or Map. */
    private boolean _mapped = false;
    
    private String[] _matches = null;
    
    /** True if the field is allowed to have nil content. */
    private boolean _nillable = false;
    
    /** The node type (attribute, element, text). */
    private NodeType _nodeType = null;
    
    /** The namespace prefix that is to be used when marshalling */
    private String _nsPrefix = null;

    /** The namespace URI used for both marshalling and unmarshalling. */
    private String _nsURI = null;

    /** The "user-set" properties of this XMLFieldDescriptor. */
    private Properties _properties = null;
    
    /** The XML Schema type of this field value. */
    private String _schemaType = null;
    
    /** The prefix used in case the value of the field described is of type QName. */
    private String _qNamePrefix = null;

    /** A flag which indicates the parent class' namespace should be used by default. */
    private boolean _useParentClassNamespace = false;
    
    private FieldValidator _validator = null;
    
    /** The XML name of the field, this is only the local name. */
    private String _xmlName    = null;
    
    /** The relative XML path used when wrapping in nested elements, does not include
     *  the name of the field itself. */
    private String _xmlPath    = null;
    
    //----------------/
    //- Constructors -/
    //----------------/

    public XMLFieldDescriptorImpl(final Class fieldType, final String fieldName,
            final String xmlName, final NodeType nodeType) {
        _matches = new String[0];

        if (fieldName == null) { throw new IllegalArgumentException(NULL_NAME_ERR); }
        if (fieldType == null) { throw new IllegalArgumentException(NULL_CLASS_ERR); }

        setFieldName(fieldName);

        if (fieldType == org.exolab.castor.types.AnyNode.class) {
            // if the field type is an AnyNode Castor must treat it as
            // an object to avoid changes in the Marshalling framework
            setFieldType(java.lang.Object.class);
        } else {
            setFieldType(fieldType);
        }
        
        _nodeType = ((nodeType == null) ? NodeType.Attribute : nodeType );
        
        //-- call the setXMLName method to handle checking for full path
        setXMLName(xmlName);
    }

    /**
     * Construct a new field descriptor for the specified field. This is
     * an XML field descriptor wrapping a field descriptor and adding XML
     * related properties and methods.
     *
     * @param fieldDesc The field descriptor
     * @param xmlName The XML name of the field
     * @param nodeType The node type of this field
     * @throws MappingException Invalid mapping information
     */
    public XMLFieldDescriptorImpl(final FieldDescriptor fieldDesc, final String xmlName,
            final NodeType nodeType, final NodeType primitiveNodeType )
    throws MappingException {
        _matches = new String[0];

	    if (fieldDesc instanceof XMLFieldDescriptor) {
            setContainingClassDescriptor(fieldDesc.getContainingClassDescriptor());
        }

        setFieldName(fieldDesc.getFieldName());

        if (fieldDesc.getFieldType() == org.exolab.castor.types.AnyNode.class) {
            // if the field type is an AnyNode Castor must treat it as
            // an object to avoid changes in the Marshalling framework
            setFieldType(java.lang.Object.class);
        } else {
            setFieldType(fieldDesc.getFieldType());
        }

        ClassDescriptor cd = fieldDesc.getClassDescriptor();
        if (cd != null) {
            if (cd instanceof XMLClassDescriptor) {
                setClassDescriptor(cd);
            } else {
                setClassDescriptor(new XMLClassDescriptorAdapter(cd, null, primitiveNodeType));
            }
        }

        setHandler(fieldDesc.getHandler());
        // Check for instances of java.util.Date. This logic really doesn't belong here,
        // as it can interfere with user specified handlers. Instead it should go into
        // XMLMappingLoader.
        if (getFieldType() != null) {
            if (java.util.Date.class.isAssignableFrom(getFieldType())) {
                if (getHandler() instanceof FieldHandlerImpl) {
                    setHandler(new DateFieldHandler(getHandler()));
                }
            }
        }
        
        setTransient(fieldDesc.isTransient());
        setImmutable(fieldDesc.isImmutable());
        setRequired(fieldDesc.isRequired());
        setMultivalued(fieldDesc.isMultivalued());


        //-- handle xml name
        if (xmlName == null) {
            setXMLName(getFieldName());
        } else {
            setXMLName(xmlName);
        }

        if (nodeType == null) {
            if (isMultivalued()) {
                _nodeType = NodeType.Element;
            } else {
                _nodeType = NodeType.Attribute;
            }
        } else {
            _nodeType = nodeType;
        }

        if (isRequired()) {
            _validator = new FieldValidator();
            _validator.setMinOccurs(1);
            _validator.setDescriptor(this);
        }
    }

    //------------------/
    //- Public Methods -/
    //------------------/

    /**
     * Sets whether or not the value of the field represented by this FieldDescriptor
     * should be set via the constructor of the containing ClassDescriptor. The index
     * value greater than 0 specifies the index within the argument array that the value
     * of this field should be.
     *
     * Note: This only applies to attribute mapped fields at this time.
     *
     * @param index the index within the argument array. A value less than zero
     *        indicates that this field should not be part of the constructor arguments.
     */
    public void setConstructorArgumentIndex(final int index) {
        if (_nodeType != NodeType.Attribute) {
            String err = "constructor arguments only valid for attribute mapped fields.";
            throw new IllegalStateException(err);
        }
        _argIndex = index;
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getConstructorArgumentIndex()
     * {@inheritDoc}
     */
    public int getConstructorArgumentIndex() {
        return _argIndex;
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#isConstructorArgument()
     * {@inheritDoc}
     */
    public boolean isConstructorArgument() {
        return (_argIndex >= 0);
    }
    
    /**
     * Sets the location path for the field being described.
     *
     * In most cases, this isn't needed. However sometimes a field may be mapped to a
     * nested element. In which case the value of the location path should be the nested
     * element name. If more than one level of nesting is needed each nested element
     * name should be separated by a path separator (forward slash '/').
     *
     * The location path name is "relative" to the parent Class. The name of the parent
     * should not be included in the path.
     * 
     * For example, give the following two classes:
     * <code>
     *    class Root {    
     *        Bar bar;    
     *    }
     *
     *    class Bar {
     *       String value;
     *    }
     * </code>
     *
     * And the following XML:
     *
     * <code>
     *    &lt;root&gt;
     *       &lt;foo&gt;
     *          &lt;bar&gt; value of bar &lt;/bar&gt;
     *       &lt;/foo&gt;
     *    &lt;/root&gt;
     * </code>
     *
     * Since foo has no associated class, the path for 'bar' would be: "foo"
     *
     * @param path the "relative" location path for the field.
     * @see #getLocationPath
     */
    public void setLocationPath(final String path) {
        //-- need to add some validation to the path at some point.
        _xmlPath = path;
    }
    
    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getLocationPath()
     * {@inheritDoc}
     */
    public String getLocationPath() {
        return _xmlPath;
    }
    
    /**
     * Sets the namespace prefix used when marshalling as XML.
     * 
     * @param nsPrefix The namespace prefix used when marshalling the "described" object.
     */
    public void setNameSpacePrefix(final String nsPrefix) {
        _nsPrefix = nsPrefix;
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getNameSpacePrefix()
     * {@inheritDoc}
     */
    public String getNameSpacePrefix() {
        return _nsPrefix;
    }

    /**
     * Sets whether or not the namespace for the parent "containing" 
     * class should be used during marshalling/unmarshalling when
     * no specific namespace URI has been set for this field.
     */
    public void setUseParentsNamespace(final boolean useParentsNamespace) {
        _useParentClassNamespace = useParentsNamespace;
    }
    
    /**
     * Sets the namespace URI used when marshalling and unmarshalling as XML.
     * 
     * @param nsURI The namespace URI used when marshalling and unmarshalling the
     *        "described" Object.
     */
    public void setNameSpaceURI(final String nsURI) {
        _nsURI = nsURI;
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getNameSpaceURI()
     * {@inheritDoc}
     */
    public String getNameSpaceURI() {
        ClassDescriptor parent = getContainingClassDescriptor();
        if ((_nsURI == null) && (parent != null) && _useParentClassNamespace) {
            Class type = getFieldType();
            boolean test = isPrimitive(type) || isBuiltInType(type) || isMappedItem(type);
            if ((_nodeType == NodeType.Element) && test) {
                if (parent instanceof XMLClassDescriptor) {
                    return ((XMLClassDescriptor) parent).getNameSpaceURI();
                }
            }
        }
        return _nsURI;
    }

    /**
     * Sets the XML node type for the described field.
     *
     * @param nodeType the NodeType for the described field.
     */
    public void setNodeType(final NodeType nodeType) {
        _nodeType = ((nodeType == null) ? NodeType.Attribute : nodeType);
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getNodeType()
     * {@inheritDoc}
     */
    public NodeType getNodeType() {
        return _nodeType;
    }

    /**
     * Sets the value property with the given name.
     *
     * @param propertyName The name of the property to set the value of.
     * @param value The value of the property.
     * @see #getProperty
     */
    public void setProperty(final String propertyName, final String value) {
        if (propertyName == null) {
            String err = "The argument 'propertyName' must not be null.";
            throw new IllegalArgumentException(err);
        }
        
        if (_properties == null) { _properties = new Properties(); }
         
        if (value == null) {
            _properties.remove(propertyName);
        } else {
            _properties.put(propertyName, value);
        }
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getProperty(java.lang.String)
     * {@inheritDoc}
     */
    public String getProperty(final String propertyName) {
        if ((_properties == null) || (propertyName == null)) { return null; }
        return _properties.getProperty(propertyName);
    }
    
    /**
     * Sets the type of the XML Schema type of the value for the field being described.
     * 
     * @param schemaType The value type.
     */
    public void setSchemaType(final String schemaType) {
        _schemaType =schemaType;
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getSchemaType()
     * {@inheritDoc}
     */
    public String getSchemaType() {
         return _schemaType;
     }

    public void setValidator(final FieldValidator validator) {
        if (_validator != null) { _validator.setDescriptor(null); }
        _validator = validator;
        if (_validator != null) { _validator.setDescriptor(this); }
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getValidator()
     * {@inheritDoc}
     */
    public FieldValidator getValidator() {
        return _validator;
    }

    /**
     * Sets the xml name for the described field.
     *
     * @param xmlName the XML name for the described field.
     */
    public void setXMLName(final String xmlName) {
        _xmlName = xmlName;
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#getXMLName()
     * {@inheritDoc}
     */
    public String getXMLName() {
        return _xmlName;
    }

    /**
     * Set if the field is a container field or not.
     *
     * @param isContainer a boolean indicating whether or not the field is a container
     *        field.
     */
    public void setContainer(final boolean isContainer) {
        _container = isContainer;
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#isContainer()
     * {@inheritDoc}
     */
    public boolean isContainer() {
        return _container;
    }
    
    /**
     * Sets the incremental flag which indicates whether this member
     * can be added before the unmarshaller is finished unmarshalling it.
     * 
     * @param incremental the boolean which if true indicated that this
     * member can safely be added before the unmarshaller is finished
     * unmarshalling it.
     */
    public void setIncremental(final boolean incremental) {
        _incremental = incremental;
    }
    
    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#isIncremental()
     * {@inheritDoc}
     */
    public boolean isIncremental() {
        return _incremental;
    }

    /**
     * Sets whether or not this field has been mapped in a Map or Hashtable.
     *
     * @param mapped a boolean that when true indicates this field is a Hashtable or Map.
     */
    public void setMapped(final boolean mapped) {
        _mapped = mapped;
    }
    
    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#isMapped()
     * {@inheritDoc}
     */
    public boolean isMapped() {
        return _mapped;
    }

    /**
     * Sets whether or not the described field is allowed to be nil. A nillable field
     * can have empty content (text or element content), but may have attribute values,
     * and still be considered value, even if the child elements are required.
     * 
     * @param nillable a boolean indicating whether or not the described field may be
     *        nillable.
     */
    public void setNillable(final boolean nillable) {
        _nillable = nillable;
    }
    
    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#isNillable()
     * {@inheritDoc}
     */
    public boolean isNillable() {
        return _nillable;
    }
    
    /**
     * Sets the flag indicating that the field described by this descriptor is a
     * reference to another field in the object model.
     *
     * @param isReference true if the field is a reference to another field.
     */
    public void setReference(final boolean isReference) {
        _isReference = isReference;
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#isReference()
     * {@inheritDoc}
     */
    public boolean isReference() {
        return _isReference;
    }

    /**
     * Sets the prefix used in case the value of the field described by this descriptor
     * is of type QName.
     * 
     * @param qNamePrefix
     */
    public void setQNamePrefix(final String qNamePrefix) {
        _qNamePrefix = qNamePrefix;
    }

    /**
     * Returns the prefix used in case the value of the field described by this
     * descriptor is of type QName. This is helpful for the Marshaller but not mandatory.
     *
     * @return the prefix used in the QName value.
     */
    public String getQNamePrefix() {
        return _qNamePrefix;
    }

    /**
     * This is a space separated list of xml names that this Field descriptor matches.
     * A '*' is wild.
     * 
     * @param matchExpr the space separated list of xml names, matched by this
     *        descriptor.
     */
    public void setMatches(String matchExpr) {
        _isWild = false;
        
        if ((matchExpr == null) || (matchExpr.length() == 0)) { return; }

        StringTokenizer st = new StringTokenizer(matchExpr);
        ArrayList names = new ArrayList();
        while (st.hasMoreTokens()) {
            String token = st.nextToken();
            if (WILD_CARD.equals(token)) {
                _isWild = true;
                break;
            }
            names.add(token);
        }
        _matches = new String[names.size()];
        names.toArray(_matches);
    }

    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#matches(java.lang.String)
     * {@inheritDoc}
     */
    public boolean matches(final String xmlName) {
        if (xmlName != null) {
            if (_isWild) {
                return true;
            } else if (_matches.length > 0) {
                for (int i = 0; i < _matches.length; i++) {
                    if (xmlName.equals(_matches[i])) { return true; }
                }
            } else {
                return xmlName.equals(_xmlName);
            }
        }
        return false;
    }
    
    /**
     * @see org.exolab.castor.xml.XMLFieldDescriptor#matches(java.lang.String, java.lang.String)
     * {@inheritDoc}
     */
    public boolean matches(final String xmlName, final String namespace) {
        // compare namespaces
        if (namespace == null) {
            if ((_nsURI != null) && (_nsURI.length() > 0)) { return false; }
        } else if (_nsURI == null) {
            if ((namespace.length() > 0) && (!_isWild)) { return false; }
        } else if (!_nsURI.equals(namespace)) {
            return false;
        }
        
        // if we make this far the namespaces match, now compare names
        return matches(xmlName);
    }
    
    /**
     * Returns true if two XMLFieldDescriptors should be treated as
     * equal. Any XMLFieldDescriptor that handles the same field
     * is considered equal.
     * @return true if two XMLFieldDescriptors should be treated as equal.
     */
    public boolean equals(final Object obj) {
        if (obj == this) return true;
        
        if ((obj == null) || (!(obj instanceof XMLFieldDescriptor))) { return false; }

        XMLFieldDescriptor descriptor = (XMLFieldDescriptor) obj;
        
        // check field names
        if (!getFieldName().equals(descriptor.getFieldName())) { return false; }
        
        // check field types
        if (!getFieldType().equals(descriptor.getFieldType())) { return false; }
        
        // check field handler
        FieldHandler tmpHandler = descriptor.getHandler();
        if (getHandler() == null) {
            return (tmpHandler == null);
        } else if (tmpHandler == null) {
            return false;
        }
               
        // The following line causes some issues when used against a FieldHandlerImpl
        // because the equals method for FieldHandlerImpl is the default. Temporary
        // replace it with a slightly more generic comparison but this should probably
        // change in the future. (kv)
        // return (_handler.equals(tmpHandler));
        return (getHandler().getClass().isInstance(tmpHandler));
    }

    /**
     * Returns the hashCode for this XMLFieldDescriptor
     * @return the hashCode for this XMLFieldDescriptor
     */
    public int hashCode() {
        int hash = 17;
        hash = 17 * getFieldName().hashCode();
        hash = hash * 17 * getFieldType().hashCode();
        if (getHandler() != null) { hash = hash * 17 * getHandler().hashCode(); }
        return hash;
    }
    
    public String toString() {
        return "XMLFieldDesciptor: " + getFieldName() + " AS " + _xmlName;
    }

    //-------------------/
    //- Private Methods -/
    //-------------------/

    /**
     * Returns true if the given class should be treated as a primitive type. This
     * method will return true for all Java primitive types, the set of primitive object
     * wrappers, as well as Strings.
     *
     * @return true If the given class should be treated as a primitive type.
     */
    private static boolean isPrimitive(final Class type) {
        if (type == null) { return false; }
        if (type.isPrimitive()) { return true; }
        if (type == String.class) { return true; }
        if ((type == Boolean.class) || (type == Character.class)) { return true; }

        // Any class which extends Number should be treated as a primitive.
        return (type.getSuperclass() == Number.class);
    }
    
    /**
     * Return true if the given class is a "built-in" type. A built-in type is one in
     * which Castor provides the default descriptor for.
     * 
     * @param type The class to check.
     * @return true If the given class is a built-in type.
     */
    private static boolean isBuiltInType(final Class type) {
        if (type == null) { return false; }
        //-- All built-in Java types, such as java.util.Date,
        //-- java.sql.Date, various Collection classes, etc.
        return (CoreDescriptors.getDescriptor(type) != null);
    }

    private static boolean isMappedItem(final Class fieldType) {
        return (fieldType == MapItem.class);
    }
}

