/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: JDOMappingLoader.java 8749 2010-09-16 08:50:29Z wguttmn $
 */
package org.exolab.castor.jdo.engine;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.cache.Cache;
import org.castor.cache.simple.CountLimited;
import org.castor.cache.simple.TimeLimited;
import org.castor.core.util.AbstractProperties;
import org.castor.core.util.Messages;
import org.castor.cpa.CPAProperties;
import org.castor.cpa.persistence.convertor.AbstractSimpleTypeConvertor;
import org.castor.cpa.persistence.convertor.TypeConvertorRegistry;
import org.castor.cpa.util.classresolution.command.ClassResolutionByFile;
import org.castor.cpa.util.classresolution.nature.ClassLoaderNature;
import org.castor.jdo.engine.SQLTypeInfos;
import org.castor.mapping.BindingType;
import org.exolab.castor.jdo.engine.nature.ClassDescriptorJDONature;
import org.exolab.castor.jdo.engine.nature.FieldDescriptorJDONature;
import org.exolab.castor.mapping.AccessMode;
import org.exolab.castor.mapping.ClassDescriptor;
import org.exolab.castor.mapping.CollectionHandler;
import org.exolab.castor.mapping.ExtendedFieldHandler;
import org.exolab.castor.mapping.FieldDescriptor;
import org.exolab.castor.mapping.FieldHandler;
import org.exolab.castor.mapping.GeneralizedFieldHandler;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.mapping.TypeConvertor;
import org.exolab.castor.mapping.loader.FieldDescriptorImpl;
import org.exolab.castor.mapping.loader.AbstractMappingLoader;
import org.exolab.castor.mapping.loader.ClassDescriptorImpl;
import org.exolab.castor.mapping.loader.CollectionHandlers;
import org.exolab.castor.mapping.loader.FieldHandlerFriend;
import org.exolab.castor.mapping.loader.FieldHandlerImpl;
import org.exolab.castor.mapping.loader.TypeInfo;
import org.exolab.castor.mapping.loader.Types;
import org.exolab.castor.mapping.xml.CacheTypeMapping;
import org.exolab.castor.mapping.xml.ClassMapping;
import org.exolab.castor.mapping.xml.FieldMapping;
import org.exolab.castor.mapping.xml.KeyGeneratorDef;
import org.exolab.castor.mapping.xml.MappingRoot;
import org.exolab.castor.mapping.xml.NamedNativeQuery;
import org.exolab.castor.mapping.xml.NamedQuery;
import org.exolab.castor.mapping.xml.Param;
import org.exolab.castor.mapping.xml.Sql;
import org.exolab.castor.mapping.xml.types.SqlDirtyType;
import org.exolab.castor.persist.spi.PersistenceFactory;

/**
 * A JDO implementation of mapping helper. Creates JDO class descriptors
 * from the mapping file.
 *
 * @author <a href="mailto:arkin AT intalio DOT com">Assaf Arkin</a>
 * @author <a href="mailto:ralf DOT joachim AT syscon DOT eu">Ralf Joachim</a>
 * @version $Revision: 8749 $ $Date: 2006-04-13 07:37:49 -0600 (Thu, 13 Apr 2006) $
 */
public final class JDOMappingLoader extends AbstractMappingLoader {

    /** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta Commons
     *  Logging </a> instance used for all logging. */
    private static final Log LOG = LogFactory.getLog(JDOMappingLoader.class);

    /** Separators between type name and parameter, e.g. "char[01]". */
    private static final char LEFT_PARAM_SEPARATOR = '[';

    /** Separators after parameter, e.g. "char[01]". */
    private static final char RIGHT_PARAM_SEPARATOR = ']';

    /**
     * Extracts parameter for type convertor from the SQL type definition of the
     * form "SQL_TYPE_NAME[PARAMETER]". If the type is not parameterized, returns
     * null.
     *
     * @param sqlTypeDef SQL type definition (e.g. char[01]).
     * @return Parameter (e.g. "01") or null if not parameterized.
     */
    public static String definition2param(final String sqlTypeDef) {
        int left = sqlTypeDef.indexOf(LEFT_PARAM_SEPARATOR);
        int right = sqlTypeDef.indexOf(RIGHT_PARAM_SEPARATOR);
        if (right < 0) {
            right = sqlTypeDef.length();
        }
        if (left < 0) {
            return null;
        }
        return sqlTypeDef.substring(left + 1, right);
    }

    /**
     * Extracts SQL type name from the the SQL type definition of the form
     * "SQL_TYPE_NAME[PARAMETER]".
     *
     * @param sqlTypeDef SQL type definition (e.g. char[01]).
     * @return SQL type name (e.g. "char").
     */
    public static String definition2type(final String sqlTypeDef) {
        int sep = sqlTypeDef.indexOf(LEFT_PARAM_SEPARATOR);
        if (sep < 0) {
            return sqlTypeDef;
        }
        return sqlTypeDef.substring(0, sep);
    }

    private final TypeConvertorRegistry _typeConvertorRegistry;

    /** 
     * Map of key generator descriptors associated by their name. 
     */
    private final Map<String, KeyGeneratorDescriptor> _keyGeneratorDescriptors
        = new HashMap<String, KeyGeneratorDescriptor>();

    /** Set of names of all named queries to identify duplicate names. */    
    private final Set<String> _queryNames = new HashSet<String>();

    /** 
     * The JDO {@link PersistenceFactory} is used for adjusting SQL type for
     * the given database. 
     */
    private PersistenceFactory _factory;

    /**
     * Creates an instance of {@link JDOMappingLoader}, providing a 
     * {@link ClassLoader} instance.
     * @param loader A Custom {@link ClassLoader} instance.
     */
    public JDOMappingLoader(final ClassLoader loader) {
        super(loader);

        AbstractProperties properties = CPAProperties.getInstance();
        _typeConvertorRegistry = new TypeConvertorRegistry(properties);
    }
    
    /**
     * {@inheritDoc}
     */
    public BindingType getBindingType() { 
        return BindingType.JDO; 
    }
    
    /**
     * {@inheritDoc}
     */
    public void loadMapping(final MappingRoot mapping, final Object param)
    throws MappingException {
        if (loadMapping()) {
            _factory = (PersistenceFactory) param;
            
            createKeyGenDescriptors(mapping);
            createClassDescriptors(mapping);
        }
    }

    /**
     * Load key generator definitions, check for duplicate definitions and convert them
     * to key generator descriptors.
     * 
     * @param mapping Mapping to load key generator defintions from.
     * @throws MappingException If mapping contains more then one key generator
     *        definition with same name.
     */
    private void createKeyGenDescriptors(final MappingRoot mapping)
    throws MappingException {
        Enumeration enumeration = mapping.enumerateKeyGeneratorDef();
        while (enumeration.hasMoreElements()) {
            KeyGeneratorDef def = (KeyGeneratorDef) enumeration.nextElement();
            
            // resolve name of the key generator definition; if there's an alias
            // defined, use it; if not, fall back to the name attribute (this
            // is to ensure that for one key generator type (e.g. HIGH-LOW), it is 
            // possible to define more than one instance with individual
            // configuration(s).
            String name = def.getAlias();
            if (name == null) { 
                name = def.getName(); 
            }
            
            KeyGeneratorDescriptor desc = _keyGeneratorDescriptors.get(name);
            if (desc != null) {
                throw new MappingException(Messages.format("mapping.dupKeyGen", name));
            }
            
            Properties params = new Properties();
            
            Enumeration enumerateParam = def.enumerateParam();
            while (enumerateParam.hasMoreElements()) {
                Param par = (Param) enumerateParam.nextElement();
                params.put(par.getName(), par.getValue());
            }
            
            desc = new KeyGeneratorDescriptor(
                    name, def.getName(), params);
            
            _keyGeneratorDescriptors.put(name, desc);
        }
    }
    
    protected ClassDescriptor createClassDescriptor(final ClassMapping classMapping)
    throws MappingException {
        // If there is no SQL information for class, ignore it.
        if ((classMapping.getMapTo() == null) || (classMapping.getMapTo().getTable() == null)) {
            LOG.info(Messages.format("mapping.ignoringMapping", classMapping.getName()));
            return null;
        }

        // Create the class descriptor, and register the JDO nature with it.
        ClassDescriptorImpl clsDesc = new ClassDescriptorImpl();
        clsDesc.addNature(ClassDescriptorJDONature.class.getName());
        ClassDescriptorJDONature jdoNature = new ClassDescriptorJDONature(clsDesc);
        
        // Obtain the Java class.
        Class<?> javaClass = resolveType(classMapping.getName());
        if (!Types.isConstructable(javaClass, true)) {
            throw new MappingException(
                    "mapping.classNotConstructable", javaClass.getName());
        }
        clsDesc.setJavaClass(javaClass);
        
        // If this class extends another class, we need to obtain the extended
        // class and make sure this class indeed extends it.
        ClassDescriptor extDesc = getExtended(classMapping, javaClass);
        if (extDesc != null) {
            if (!(extDesc.hasNature(ClassDescriptorJDONature.class.getName()))) {
                throw new IllegalArgumentException(
                        "Extended class does not have a JDO descriptor");
            }
            
            new ClassDescriptorJDONature(extDesc).addExtended(clsDesc);
        }
        clsDesc.setExtends(extDesc);
        
        // If this class depends on another class, obtain the class it depends upon.
        clsDesc.setDepends(getDepended(classMapping, javaClass));
        
        // Create all field descriptors.
        FieldDescriptorImpl[] allFields = createFieldDescriptors(classMapping, javaClass);

        // Make sure there are no two fields with the same name.
        checkFieldNameDuplicates(allFields, javaClass);

        // Set class descriptor containing the field
        for (int i = 0; i < allFields.length; i++) {
            allFields[i].setContainingClassDescriptor(clsDesc);
        }
        
        // Identify identity and normal fields. Note that order must be preserved.
        List<FieldDescriptor> fieldList = new ArrayList<FieldDescriptor>(allFields.length);
        List<FieldDescriptor> idList = new ArrayList<FieldDescriptor>();
        if (extDesc == null) {
            // Sort fields into 2 lists based on identity definition of field.
            for (int i = 0; i < allFields.length; i++) {
                if (!allFields[i].isIdentity()) {
                    fieldList.add(allFields[i]);
                } else {
                    idList.add(allFields[i]);
                }
            }
            
            if (idList.size() == 0) {
                // Found no identities based on identity definition of field.
                // Try to find identities based on identity definition on class.
                String[] idNames = classMapping.getIdentity();
                if ((idNames == null) || (idNames.length == 0)) {
                    // There are also no identity definitions on class.
                    throw new MappingException("mapping.noIdentity", javaClass.getName());
                }

                FieldDescriptor identity;
                for (int i = 0; i < idNames.length; i++) {
                    identity = findIdentityByName(fieldList, idNames[i], javaClass);
                    if (identity != null) {
                        idList.add(identity);
                    } else {
                        throw new MappingException("mapping.identityMissing",
                                idNames[i], javaClass.getName());
                    }
                }
            }
        } else {
            // Add all fields of extending class to field list.
            for (int i = 0; i < allFields.length; i++) { fieldList.add(allFields[i]); }
            
            // Add all identities of extended class to identity list.
            FieldDescriptor[] extIds = ((ClassDescriptorImpl) extDesc).getIdentities();
            for (int i = 0; i < extIds.length; i++) { idList.add(extIds[i]); }
            
            // Search redefined identities in extending class.
            FieldDescriptor identity;
            for (int i = 0; i < idList.size(); i++) {
                String idName = (idList.get(i)).getFieldName();
                identity = findIdentityByName(fieldList, idName, javaClass);
                if (identity != null) { idList.set(i, identity); }
            }
        }
        
        // Set identities on class descriptor.
        FieldDescriptor[] ids = new FieldDescriptor[idList.size()];
        clsDesc.setIdentities(idList.toArray(ids));

        // Set fields on class descriptor.
        FieldDescriptor[] fields = new FieldDescriptor[fieldList.size()];
        clsDesc.setFields(fieldList.toArray(fields));
        
        jdoNature.setTableName(classMapping.getMapTo().getTable());
        
        // Set the field name used for object modification checks.
        jdoNature.setVersionField(classMapping.getVersion());

        extractAndSetAccessMode(jdoNature, classMapping);
        extractAndAddCacheParams(jdoNature, classMapping, javaClass);
        extractAndAddNamedQueries(jdoNature, classMapping);
        extractAndAddNamedNativeQueries(jdoNature, classMapping);
        extractAndSetKeyGeneratorDescriptor(jdoNature, classMapping.getKeyGenerator());

        return clsDesc;
    }

    /**
     * Extract access mode from class mapping and set it at JDO class descriptor.
     * 
     * @param jdoNature JDO class descriptor to set the access mode on.
     * @param clsMap Class mapping to extract the access mode from.
     */
    private void extractAndSetAccessMode(final ClassDescriptorJDONature jdoNature,
            final ClassMapping clsMap) {
        if (clsMap.getAccess() != null) {
            jdoNature.setAccessMode(AccessMode.valueOf(clsMap.getAccess().toString()));
        }
    }
    
    /**
     * Extract cache parameters from class mapping and add them to JDO class descriptor.
     * 
     * @param jdoNature JDO class descriptor to add the cache parameters to.
     * @param clsMap Class mapping to extract the cache parameters from.
     * @param javaClass Class the cache parameters are defined for.
     * @throws MappingException If cache type <code>none</code> has been specified for
     *         a class that implements <code>TimeStampable</code> interface.
     */
    private void extractAndAddCacheParams(final ClassDescriptorJDONature jdoNature,
            final ClassMapping clsMap, final Class javaClass)
    throws MappingException {
        jdoNature.addCacheParam(Cache.PARAM_NAME, clsMap.getName());

        CacheTypeMapping cacheMapping = clsMap.getCacheTypeMapping();
        if (cacheMapping != null) {
            String type = cacheMapping.getType();
            jdoNature.addCacheParam(Cache.PARAM_TYPE, type);
            
            Param[] params = cacheMapping.getParam();
            for (int i = 0; i < params.length; i++) {
                jdoNature.addCacheParam(params[i].getName(), params[i].getValue());
            }

            String debug = new Boolean(cacheMapping.getDebug()).toString();
            jdoNature.addCacheParam(Cache.PARAM_DEBUG, debug);

            String capacity = Long.toString(cacheMapping.getCapacity());
            jdoNature.addCacheParam(CountLimited.PARAM_CAPACITY, capacity);
            jdoNature.addCacheParam(TimeLimited.PARAM_TTL, capacity);
        }
    }
    
    /**
     * Extract named queries from class mapping and add them to JDO class descriptor.
     * 
     * @param jdoNature JDO class descriptor to add the named queries to.
     * @param clsMap Class mapping to extract the named queries from.
     * @throws MappingException On duplicate query names.
     */
    private void extractAndAddNamedQueries(final ClassDescriptorJDONature jdoNature,
            final ClassMapping clsMap)
    throws MappingException {
        Enumeration namedQueriesEnum = clsMap.enumerateNamedQuery();
        while (namedQueriesEnum.hasMoreElements()) {
            NamedQuery query = (NamedQuery) namedQueriesEnum.nextElement();
            String queryName = query.getName();
            if (_queryNames.contains(queryName)) {
                throw new MappingException(
                        "Duplicate entry for named query with name " + queryName);
            }
            _queryNames.add(queryName);

            jdoNature.addNamedQuery(queryName, query.getQuery());
        }
    }
    
    /**
     * Extract named native queries from class mapping and add them to JDO class descriptor.
     * 
     * @param jdoNature JDO class descriptor to add the named queries to.
     * @param clsMap Class mapping to extract the named native queries from.
     * @throws MappingException On duplicate query names.
     */
    private void extractAndAddNamedNativeQueries(final ClassDescriptorJDONature jdoNature,
            final ClassMapping clsMap)
    throws MappingException {
        Enumeration<? extends NamedNativeQuery> namedNativeQueriesEnum =
            clsMap.enumerateNamedNativeQuery();
        while (namedNativeQueriesEnum.hasMoreElements()) {
            NamedNativeQuery query = namedNativeQueriesEnum.nextElement();
            String queryName = query.getName();
            if (_queryNames.contains(queryName)) {
                throw new MappingException(
                        "Duplicate entry for named query with name " + queryName);
            }
            _queryNames.add(queryName);

            jdoNature.addNamedNativeQuery(queryName, query);
        }
    }
    
    /**
     * Resolves (or creates) the key generator from a given key generator name.
     * 
     * Search for an already existing key generator descriptor, e.g. those generated 
     * from the key generator definitions in mapping. If no descriptor can be found 
     * a new one is created and added to the map of class descriptors. Set the 
     * key generator descriptor at the class descriptor.
     * 
     * @param jdoNature JDO class descriptor to set the key generator descriptor at.
     * @param keyGeneratorName Name of the key generator.
     */
    private void extractAndSetKeyGeneratorDescriptor(final ClassDescriptorJDONature jdoNature,
            final String keyGeneratorName) {
        KeyGeneratorDescriptor keyGeneratorDescriptor = null;
        
        if (keyGeneratorName != null) {
            keyGeneratorDescriptor = _keyGeneratorDescriptors.get(keyGeneratorName);
            if (keyGeneratorDescriptor == null) {
                keyGeneratorDescriptor = new KeyGeneratorDescriptor(
                        keyGeneratorName, keyGeneratorName, new Properties());
                _keyGeneratorDescriptors.put(keyGeneratorName, keyGeneratorDescriptor);
            }
        }
        
        jdoNature.setKeyGeneratorDescriptor(keyGeneratorDescriptor);
    }
    
    protected FieldDescriptor findIdentityByName(
            final List<FieldDescriptor> fldList, final String idName, final Class javaClass)
    throws MappingException {
        for (int i = 0; i < fldList.size(); i++) {
            FieldDescriptor field = fldList.get(i);
            if (idName.equals(field.getFieldName())) {
                if (!(field.hasNature(FieldDescriptorJDONature.class.getName()))) {
                    throw new IllegalStateException(
                            "Identity field must be of type JDOFieldDescriptor");
                }
                
                String[] sqlName = new FieldDescriptorJDONature(field).getSQLName();
                if (sqlName == null) {
                    throw new MappingException("mapping.noSqlName",
                            field.getFieldName(), javaClass.getName());
                }

                fldList.remove(i);
                return field;
            }
        }
        return null;
    }

    /**
     * Walks through all fields of a descriptor and resolves relation
     * {@link ClassDescriptor}s by using mapping information or, if not
     * present, resolution by file to support generated
     * {@link ClassDescriptor}s. Resolved {@link ClassDescriptor}s will be
     * set as a field's descriptor.
     * 
     * @param clsDesc
     *            The ClassDescriptor in focus.
     */
    protected void resolveRelations(final ClassDescriptor clsDesc) {
        FieldDescriptor[] fields = clsDesc.getFields();
        for (int i = 0; i < fields.length; ++i) {
            FieldDescriptor field = fields[i];
            ClassDescriptor desc = getDescriptor(field.getFieldType().getName());
            // Resolve ClassDescriptor from the file system as well.
            if (desc == null && !field.getFieldType().isPrimitive()) {
                ClassResolutionByFile resolutionCommand = new ClassResolutionByFile();
                resolutionCommand.addNature(ClassLoaderNature.class.getName());
                ClassLoaderNature clNature = new ClassLoaderNature(
                        resolutionCommand);
                clNature.setClassLoader(getClassLoader());
                desc = resolutionCommand.resolve(field.getFieldType());
                ((FieldDescriptorImpl) field).setClassDescriptor(desc);
            }
            if ((desc != null) && (field instanceof FieldDescriptorImpl)) {
                ((FieldDescriptorImpl) field).setClassDescriptor(desc);
            }
        }
    }

    /**
     * Parse the sql type attribute to build an
     * array of types, needed to support whitespace inside
     * parameterized types (see Bug 1045).
     */
    protected String[] getSqlTypes(final FieldMapping fieldMap) {
        if (fieldMap.getSql() == null) { return new String[0]; }
        String sqlType = fieldMap.getSql().getType();
        if (sqlType == null) { return new String[0]; }

        ArrayList<String> types = new ArrayList<String>();
        int current = 0;
        int begin = 0;
        int state = 0;
        while (current < sqlType.length()) {
            switch (state) {
            case 0:
                if (sqlType.charAt(current) == ' ') {
                    types.add(sqlType.substring(begin, current));
                    begin = current + 1;
                } else if (sqlType.charAt(current) == '[') {
                    state = 1;
                }
                break;
            case 1:
                if (sqlType.charAt(current) == ']') {
                    state = 0;
                }
                break;
            default:
                break;
            }
            current++;
        }
        types.add(sqlType.substring(begin, current));
        String[] result = new String[types.size()];
        return types.toArray(result);
    }


    protected TypeInfo getTypeInfo(final Class fieldType, final CollectionHandler colHandler,
            final FieldMapping fieldMap) throws MappingException {
        Class internalFieldType = Types.typeFromPrimitive(fieldType);
        String typeName = null;
        Class sqlType = null;
        String sqlParam = null;

        String[] sqlTypes = getSqlTypes(fieldMap);
        if ((fieldMap.getSql() != null) && (sqlTypes.length > 0)) {
            //--TO Check
            typeName = sqlTypes[0];
            sqlType = SQLTypeInfos.sqlTypeName2javaType(definition2type(typeName));
            sqlParam = definition2param(typeName);
        } else {
            sqlType = internalFieldType;
        }
        
        if (_factory != null) {
            sqlType = _factory.adjustSqlType(sqlType);
        }
        
        TypeConvertor convertorTo = null;
        TypeConvertor convertorFrom = null;

        if (internalFieldType != sqlType) {
            try {
                convertorTo = _typeConvertorRegistry.getConvertor(
                        sqlType, internalFieldType, sqlParam);
            } catch (MappingException ex) {
                boolean isTypeSafeEnum = false;
                //-- check for type-safe enum style classes
                if ((internalFieldType != null) && (!isPrimitive(internalFieldType))) {
                    //-- make sure no default constructor
                    Constructor cons = null;
                    try {
                        cons = internalFieldType.getConstructor(EMPTY_ARGS);
                        if (!Modifier.isPublic(cons.getModifiers())) {
                            cons = null;
                        }
                    } catch (NoSuchMethodException nsmx) {
                        //-- Do nothing
                    }
                    
                    if (cons == null) {
                        //-- make sure a valueOf factory method exists
                        try {
                            Method method = internalFieldType.getMethod(VALUE_OF, STRING_ARG);
                            Class returnType = method.getReturnType();
                            if ((returnType != null)
                                    && internalFieldType.isAssignableFrom(returnType)) {
                                int mods = method.getModifiers();
                                if (Modifier.isStatic(mods)) {
                                    // create individual SQLTypeConverter
                                    convertorTo = new String2EnumTypeConvertor(
                                            sqlType, internalFieldType, method);

                                    Types.addEnumType(internalFieldType);
                                    
                                    isTypeSafeEnum = true;
                                }
                            }
                        } catch (NoSuchMethodException nsmx) {
                            //-- Do nothing
                        }
                    }
                    
                    if (isTypeSafeEnum) {
                    	//-- check if name() method should be used instead of toString()
                        try {
                            Method method = internalFieldType.getMethod(NAME);
                            if (String.class == method.getReturnType()) {
                                convertorFrom = new Enum2StringTypeConvertor(
                                		internalFieldType, sqlType, method);
                            }
                        } catch (NoSuchMethodException nsmx) {
                            //-- Do nothing
                        }
                    }
                }
                
                if (!isTypeSafeEnum) {
                    throw new MappingException("mapping.noConvertor",
                    		sqlType.getName(), internalFieldType.getName());
                }
            }
            
            if (convertorFrom == null) {
                convertorFrom = _typeConvertorRegistry.getConvertor(
                        internalFieldType, sqlType, sqlParam);
            }
            
            if ((convertorTo != null) && (convertorFrom != null)) {
                Types.addConvertibleType(internalFieldType);
            }
        }
        
        return new TypeInfo(internalFieldType, convertorTo, convertorFrom,
                            fieldMap.getRequired(), null, colHandler);
    }
    
    private class String2EnumTypeConvertor extends AbstractSimpleTypeConvertor {
        private final Method _method;

        public String2EnumTypeConvertor(final Class<?> fromType,
                final Class<?> toType, final Method method) {
            super(fromType, toType);

            _method = method;
        }

        /**
         * {@inheritDoc}
         */
        public Object convert(final Object object) {
            try {
                return _method.invoke(toType(), new Object[] {(String) object});
            } catch (Exception ex) {
                return null;
            }
        }
    }

    private class Enum2StringTypeConvertor extends AbstractSimpleTypeConvertor {
        private final Method _method;

        public Enum2StringTypeConvertor(final Class<?> fromType,
                final Class<?> toType, final Method method) {
            super(fromType, toType);

            _method = method;
        }

        /**
         * {@inheritDoc}
         */
        public Object convert(final Object object) {
            try {
                return _method.invoke(object);
            } catch (Exception ex) {
                return null;
            }
        }
    }

    protected FieldDescriptorImpl createFieldDesc(final Class javaClass,
            final FieldMapping fieldMap) throws MappingException {

        // If not an SQL field, return a stock field descriptor.
        Sql sql = fieldMap.getSql();
        if (sql == null) {
            return super.createFieldDesc(javaClass, fieldMap);
        }
        
        String fieldName = fieldMap.getName();
        
        // If the field type is supplied, grab it and use it to locate the
        // field/accessor.
        Class fieldType = null;
        if (fieldMap.getType() != null) {
            fieldType = resolveType(fieldMap.getType());
        }
        
        // If the field is declared as a collection, grab the collection type as
        // well and use it to locate the field/accessor.
        CollectionHandler colHandler = null;
        boolean isLazy = fieldMap.getLazy();
        if (fieldMap.getCollection() != null) {
            Class colType = CollectionHandlers.getCollectionType(
                    fieldMap.getCollection().toString());
            colHandler = CollectionHandlers.getHandler(colType);
            
            if (colType.getName().equals("java.util.Iterator") && isLazy) {
                String err = "Lazy loading not supported for collection type 'iterator'";
                throw new MappingException(err);
            }

            if (colType.getName().equals("java.util.Enumeration") && isLazy) {
                String err = "Lazy loading not supported for collection type 'enumerate'";
                throw new MappingException(err);
            }
        }
        
        TypeInfo typeInfo = getTypeInfo(fieldType, colHandler, fieldMap);
            
        ExtendedFieldHandler exfHandler = null;
        FieldHandler handler = null;
        
        //-- check for user supplied FieldHandler
        if (fieldMap.getHandler() != null) {
            
            Class handlerClass = resolveType(fieldMap.getHandler());
            
            if (!FieldHandler.class.isAssignableFrom(handlerClass)) {
                String err = "The class '" + fieldMap.getHandler()
                    + "' must implement " + FieldHandler.class.getName();
                throw new MappingException(err);
            }
            
            //-- get default constructor to invoke. We can't use the
            //-- newInstance method unfortunately becaue FieldHandler
            //-- overloads this method 
            Constructor constructor = null;
            try {
                constructor = handlerClass.getConstructor(new Class[0]);
                handler = (FieldHandler) 
                    constructor.newInstance(new Object[0]);
            } catch (Exception except) {
                String err = "The class '" + handlerClass.getName()
                    + "' must have a default public constructor.";
                throw new MappingException(err);
            }
            
            
            //-- ExtendedFieldHandler?
            if (handler instanceof ExtendedFieldHandler) {
                exfHandler = (ExtendedFieldHandler) handler;
            }
            
            //-- Fix for Castor JDO from Steve Vaughan, Castor JDO
            //-- requires FieldHandlerImpl or a ClassCastException
            //-- will be thrown... [KV 20030131 - also make sure this new handler 
            //-- doesn't use it's own CollectionHandler otherwise
            //-- it'll cause unwanted calls to the getValue method during
            //-- unmarshalling]
            colHandler = typeInfo.getCollectionHandler();
            typeInfo.setCollectionHandler(null);
            handler = new FieldHandlerImpl(handler, typeInfo);
            typeInfo.setCollectionHandler(colHandler);
            //-- End Castor JDO fix
            
        } 
        
        boolean generalized = (exfHandler instanceof GeneralizedFieldHandler);
        
        //-- if generalized we need to change the fieldType to whatever
        //-- is specified in the GeneralizedFieldHandler so that the
        //-- correct getter/setter methods can be found
        FieldHandler custom = handler;
        if (generalized) {
            fieldType = ((GeneralizedFieldHandler) exfHandler).getFieldType();
        }
        
        if (generalized || (handler == null)) {
            //-- create TypeInfoRef to get new TypeInfo from call
            //-- to createFieldHandler
            TypeInfoReference typeInfoRef = new TypeInfoReference();
            typeInfoRef.typeInfo = typeInfo;
            handler = createFieldHandler(javaClass, fieldType, fieldMap, typeInfoRef);
            if (custom != null) {
                ((GeneralizedFieldHandler) exfHandler).setFieldHandler(handler);
                handler = custom;
            } else {
                typeInfo = typeInfoRef.typeInfo;
            }
        }
                
        String[] sqlName = sql.getName();

        String[] sqlTypes = getSqlTypes(fieldMap);

        int[] sqlTypeNum;
        if (sqlTypes.length > 0) {
            sqlTypeNum = new int[sqlTypes.length];
            for (int i = 0; i < sqlTypes.length; i++) {
                String sqlTypeString = definition2type(sqlTypes[i]);
                Class sqlType = SQLTypeInfos.sqlTypeName2javaType(sqlTypeString);
                if (_factory != null) { sqlType = _factory.adjustSqlType(sqlType); }
                sqlTypeNum[i] = SQLTypeInfos.javaType2sqlTypeNum(sqlType);
            }
        } else {
            Class sqlType = typeInfo.getFieldType();
            if (_factory != null) { sqlType = _factory.adjustSqlType(sqlType); }
            sqlTypeNum = new int[] {SQLTypeInfos.javaType2sqlTypeNum(sqlType)};
        }

        // create FieldDescriptor(Impl) instance, and apply JDO nature
        FieldDescriptorImpl fieldDescriptor = 
            new FieldDescriptorImpl(fieldName, typeInfo, handler, fieldMap.getTransient());
        fieldDescriptor.addNature(FieldDescriptorJDONature.class.getName());
        
        fieldDescriptor.setRequired(fieldMap.getRequired());

        // If we're using an ExtendedFieldHandler we need to set the FieldDescriptor
        if (exfHandler != null) {
            ((FieldHandlerFriend) exfHandler).setFieldDescriptor(fieldDescriptor);
        }

        // if SQL mapping declares transient
        // TODO: cross-check that this should be implemented like this
        if (sql.getTransient()) {
            fieldDescriptor.setTransient(true);
        }
        
        // set collection type as specified in mapping file
        fieldDescriptor.setCollection(fieldMap.getCollection());
        
        fieldDescriptor.setComparator(fieldMap.getComparator());
        fieldDescriptor.setCreateMethod(fieldMap.getCreateMethod());
        fieldDescriptor.setGetMethod(fieldMap.getGetMethod());
        fieldDescriptor.setSetMethod(fieldMap.getSetMethod());
        fieldDescriptor.setDirect(fieldMap.getDirect());
        
        // extract values for 'laziness' from field mapping
        fieldDescriptor.setLazy(isLazy);
        
        FieldDescriptorJDONature fieldJdoNature = 
            new FieldDescriptorJDONature(fieldDescriptor);
        
        fieldJdoNature.setTypeConvertor(typeInfo.getConvertorFrom());
        if (sqlName.length > 0) {
            fieldJdoNature.setSQLName(sqlName);
        }
        fieldJdoNature.setSQLType(sqlTypeNum);
        fieldJdoNature.setManyTable(sql.getManyTable());
        if (sql.getManyKey().length > 0) {
            fieldJdoNature.setManyKey(sql.getManyKey());
        }
        fieldJdoNature.setDirtyCheck(!SqlDirtyType.IGNORE.equals(sql.getDirty()));
        fieldJdoNature.setReadOnly(sql.getReadOnly());
        
        fieldJdoNature.setCascading(sql.getCascading());
        fieldJdoNature.setTransient(sql.getTransient());
        
        return fieldDescriptor;
    }

}
