/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Rodrigo Westrupp
 */

package com.caucho.amber.cfg;

import com.caucho.amber.entity.Listener;

import com.caucho.amber.field.*;
import com.caucho.amber.idgen.IdGenerator;
import com.caucho.amber.manager.AmberPersistenceUnit;
import com.caucho.amber.table.AmberColumn;
import com.caucho.amber.table.ForeignColumn;
import com.caucho.amber.table.LinkColumns;
import com.caucho.amber.table.AmberTable;
import com.caucho.amber.type.*;
import com.caucho.bytecode.*;
import com.caucho.config.ConfigException;
import com.caucho.jdbc.JdbcMetaData;
import com.caucho.util.L10N;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import javax.persistence.*;
import javax.persistence.EmbeddedId;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * Base concrete introspector for orm.xml and annotations.
 */
public class BaseConfigIntrospector extends AbstractConfigIntrospector {
  private static final Logger log
    = Logger.getLogger(BaseConfigIntrospector.class.getName());
  private static final L10N L = new L10N(BaseConfigIntrospector.class);

  private static final Class []_annTypes = new Class[] {
    Basic.class, javax.persistence.Column.class, javax.persistence.Id.class,
    ElementCollection.class, EmbeddedId.class,
    ManyToOne.class, OneToMany.class, OneToOne.class, ManyToMany.class,
    Version.class, Transient.class
  };

  final AmberConfigManager _configManager;
  final AmberPersistenceUnit _persistenceUnit;

  ArrayList<Completion> _linkCompletions = new ArrayList<Completion>();
  ArrayList<Completion> _depCompletions = new ArrayList<Completion>();

  HashMap<String, EmbeddableConfig> _embeddableConfigMap
    = new HashMap<String, EmbeddableConfig>();

  ArrayList<EntityMappingsConfig> _entityMappingsList;

  // HashMap<String, EntityConfig> _entityConfigMap
  //   = new HashMap<String, EntityConfig>();
  //
  // HashMap<String, MappedSuperclassConfig> _mappedSuperclassConfigMap
  //   = new HashMap<String, MappedSuperclassConfig>();

  /**
   * Creates the introspector.
   */
  public BaseConfigIntrospector(AmberConfigManager manager)
  {
    _configManager = manager;
    _persistenceUnit = manager.getPersistenceUnit();
  }

  /**
   * Sets the entity mappings list.
   */
  public void setEntityMappingsList(ArrayList<EntityMappingsConfig> entityMappingsList)
  {
    _entityMappingsList = entityMappingsList;
  }

  /**
   * Returns the entity config for a class name.
   */
  public EntityConfig getEntityConfig(String className)
  {
    // jpa/0r41
    if (_entityMappingsList == null)
      return null;

    // jpa/0s2l: mapping-file.

    HashMap<String, EntityConfig> entityMap;
    EntityConfig entityConfig;

    for (EntityMappingsConfig entityMappings : _entityMappingsList) {
      entityMap = entityMappings.getEntityMap();

      if (entityMap != null) {
        entityConfig = entityMap.get(className);

        if (entityConfig != null)
          return entityConfig;
      }
    }

    return null;
  }

  /**
   * Returns the mapped superclass config for a class name.
   */
  public MappedSuperclassConfig getMappedSuperclassConfig(String className)
  {
    if (_entityMappingsList == null)
      return null;

    HashMap<String, MappedSuperclassConfig> superclassMap;
    MappedSuperclassConfig superclassConfig;

    for (EntityMappingsConfig entityMappings : _entityMappingsList) {
      superclassMap = entityMappings.getMappedSuperclassMap();

      if (superclassMap != null) {
        superclassConfig = superclassMap.get(className);

        if (superclassConfig != null)
          return superclassConfig;
      }
    }

    return null;
  }

  /**
   * Initializes the persistence unit meta data:
   * default listeners and so on.
   */
  public void initMetaData(ArrayList<EntityMappingsConfig> entityMappingsList,
                           AmberPersistenceUnit persistenceUnit)
    throws ConfigException
  {
    PersistenceUnitMetaDataConfig metaData = null;

    for (EntityMappingsConfig entityMappings : entityMappingsList) {
      metaData = entityMappings.getPersistenceUnitMetaData();

      // It is undefined if this element occurs in multiple mapping
      // files within the same persistence unit.
      if (metaData != null)
        break;
    }

    if (metaData == null)
      return;

    PersistenceUnitDefaultsConfig defaults;

    defaults = metaData.getPersistenceUnitDefaults();

    if (defaults == null)
      return;

    EntityListenersConfig entityListeners;
    entityListeners = defaults.getEntityListeners();

    if (entityListeners == null)
      return;

    ArrayList<EntityListenerConfig> listeners;
    listeners = entityListeners.getEntityListeners();

    for (EntityListenerConfig listener : listeners)
      introspectDefaultListener(listener, persistenceUnit);
  }

  public void introspectDefaultListener(EntityListenerConfig listener,
                                        AmberPersistenceUnit persistenceUnit)
    throws ConfigException
  {
    String className = listener.getClassName();

    Class type = persistenceUnit.loadTempClass(className);

    if (type == null)
      throw new ConfigException(L.l("'{0}' is an unknown type for <entity-listener> in orm.xml",
                                    className));

    ListenerType listenerType = persistenceUnit.addDefaultListener(type);

    introspectListener(type, listenerType);
  }

  public void introspectEntityListeners(Class type,
                                        EntityType entityType,
                                        AmberPersistenceUnit persistenceUnit)
    throws ConfigException
  {
    getInternalEntityListenersConfig(type, _annotationCfg);
    EntityListeners entityListenersAnn = (EntityListeners) _annotationCfg.getAnnotation();
    EntityListenersConfig entityListenersCfg
      = _annotationCfg.getEntityListenersConfig();

    Class listeners[] = null;
/*
    // XML mapping takes higher priority than annotations.
    if (entityListenersCfg != null)
      listeners = entityListenersCfg.getEntityListeners().toArray();
    else if (entityListenersAnn != null)
      listeners = entityListenersAnn.value();
    else
      return;
*/
    String entityTypeName = entityType.getBeanClass().getName();

    for (int i = 0; listeners != null && i < listeners.length; i++) {

      Class cl = null;

      // Introspects annotation or xml.
      if (listeners[i] instanceof Class)
        cl = (Class) listeners[i];
      else {
        /*
        EntityListenerConfig listenerConfig
          = (EntityListenerConfig) listeners[i];

        String className = listenerConfig.getClassName();

        cl = persistenceUnit.loadTempClass(className);

        if (cl == null)
          throw new ConfigException(L.l("'{0}' is an unknown type for <entity-listener> in orm.xml",
                                        className));
         */
      }

      if (persistenceUnit.getDefaultListener(cl.getName()) != null)
        continue;

      introspectEntityListener(cl,
                               persistenceUnit,
                               entityType,
                               entityTypeName);
    }
  }

  public void introspectEntityListener(Class type,
                                       AmberPersistenceUnit persistenceUnit,
                                       EntityType sourceType,
                                       String sourceClassName)
    throws ConfigException
  {
    if (type == null) {
      throw new ConfigException(L.l("'{0}' is an unknown type for @EntityListeners annotated at class '{1}'",
                                    type.getName(),
                                    sourceClassName));
    }

    Class parentClass = type.getSuperclass();

    if (parentClass == null) {
      // java.lang.Object
      return;
    }
    /*
    else {
      // XXX: entity listener super-classes in a hierarchy might
      // not be annotated as entity listeners but they might have
      // @PreXxx or @PostXxx annotated methods. On the other hand,
      // needs to filter regular classes out.

      introspectEntityListener(parentClass, persistenceUnit,
                               sourceType, sourceClassName);
    }
    */

    // jpa/0r42

    ListenerType listenerType
      = persistenceUnit.getEntityListener(type.getName());

    ListenerType newListenerType
      = persistenceUnit.addEntityListener(sourceClassName, type);

    if (listenerType == null) {
      listenerType = newListenerType;
      introspectListener(type, listenerType);
    }

    sourceType.addListener(listenerType);
  }

  public void introspectListener(Class type,
                                 ListenerType listenerType)
    throws ConfigException
  {
    listenerType.setInstanceClassName(listenerType.getName() + "__ResinExt");

    for (Method method : type.getDeclaredMethods()) {
      introspectCallbacks(listenerType, method);
    }
  }

  /**
   * Introspects the callbacks.
   */
  public void introspectCallbacks(Class type,
                                  EntityType entityType)
    throws ConfigException
  {
    getInternalExcludeDefaultListenersConfig(type, _annotationCfg);

    if (! _annotationCfg.isNull())
      entityType.setExcludeDefaultListeners(true);

    getInternalExcludeSuperclassListenersConfig(type, _annotationCfg);

    if (! _annotationCfg.isNull())
      entityType.setExcludeSuperclassListeners(true);

    for (Method method : type.getDeclaredMethods()) {
      introspectCallbacks(entityType, method);
    }
  }

  /**
   * Introspects the callbacks.
   */
  public void introspectCallbacks(AbstractEnhancedType type,
                                  Method method)
    throws ConfigException
  {
    Class []param = method.getParameterTypes();

    String methodName = method.getName();
    Class jClass = type.getBeanClass();

    boolean isListener = type instanceof ListenerType;

    int n = ListenerType.CALLBACK_CLASS.length;

    for (int i = 1; i < n; i++) {
      getInternalCallbackConfig(i, jClass, method, methodName,
                                _annotationCfg);

      if (! _annotationCfg.isNull()) {
        validateCallback(ListenerType.CALLBACK_CLASS[i].getName(),
                         method, isListener);

        type.addCallback(i, method);
      }
    }
  }

  /**
   * Introspects named queries.
   */
  void introspectNamedQueries(Class type, String typeName)
  {
    // jpa/0y0-

    getInternalNamedQueryConfig(type, _annotationCfg);
    NamedQuery namedQueryAnn = (NamedQuery) _annotationCfg.getAnnotation();
    NamedQueryConfig namedQueryConfig = _annotationCfg.getNamedQueryConfig();

    // getInternalNamedQueriesConfig(type);
    NamedQueries namedQueriesAnn = (NamedQueries) type.getAnnotation(NamedQueries.class);
    // NamedQueriesConfig namedQueriesConfig = _annotationCfg.getNamedQueriesConfig();

    if ((namedQueryAnn == null) && (namedQueriesAnn == null))
      return;

    NamedQuery namedQueryArray[];

    if ((namedQueryAnn != null) && (namedQueriesAnn != null)) {
      throw new ConfigException(L.l("{0} may not have both @NamedQuery and @NamedQueries",
                                    typeName));
    }
    else if (namedQueriesAnn != null) {
      namedQueryArray = namedQueriesAnn.value();
    }
    else {
      namedQueryArray = new NamedQuery[] { namedQueryAnn };
    }

    for (int i=0; i < namedQueryArray.length; i++) {
      namedQueryAnn = namedQueryArray[i];
      _persistenceUnit.addNamedQuery(namedQueryAnn.name(),
                                     namedQueryAnn.query());
    }
  }

  /**
   * Introspects named native queries.
   */
  void introspectNamedNativeQueries(Class type, String typeName)
  {
    // jpa/0y2-

    getInternalNamedNativeQueryConfig(type, _annotationCfg);
    NamedNativeQuery namedNativeQueryAnn = (NamedNativeQuery) _annotationCfg.getAnnotation();
    NamedNativeQueryConfig namedNativeQueryConfig = _annotationCfg.getNamedNativeQueryConfig();

    NamedNativeQueries namedNativeQueriesAnn = (NamedNativeQueries) type.getAnnotation(NamedNativeQueries.class);

    if ((namedNativeQueryAnn == null) && (namedNativeQueriesAnn == null))
      return;

    NamedNativeQuery namedNativeQueryArray[];

    if ((namedNativeQueryAnn != null) && (namedNativeQueriesAnn != null)) {
      throw new ConfigException(L.l("{0} may not have both @NamedNativeQuery and @NamedNativeQueries",
                                    typeName));
    }
    else if (namedNativeQueriesAnn != null) {
      namedNativeQueryArray = namedNativeQueriesAnn.value();
    }
    else {
      namedNativeQueryArray = new NamedNativeQuery[] { namedNativeQueryAnn };
    }

    for (int i=0; i < namedNativeQueryArray.length; i++) {
      namedNativeQueryAnn = namedNativeQueryArray[i];

      NamedNativeQueryConfig nativeQueryConfig = new NamedNativeQueryConfig();

      nativeQueryConfig.setQuery(namedNativeQueryAnn.query());
      nativeQueryConfig.setResultClass(namedNativeQueryAnn.resultClass());
      nativeQueryConfig.setResultSetMapping(namedNativeQueryAnn.resultSetMapping());
      _persistenceUnit.addNamedNativeQuery(namedNativeQueryAnn.name(),
                                           nativeQueryConfig);
    }
  }

  /**
   * Introspects sql result set mappings.
   */
  void introspectSqlResultSetMappings(Class type,
                                      EntityType entityType,
                                      String typeName)
  {
    // jpa/0y1-

    getInternalSqlResultSetMappingConfig(type, _annotationCfg);
    SqlResultSetMapping sqlResultSetMappingAnn = (SqlResultSetMapping) _annotationCfg.getAnnotation();
    SqlResultSetMappingConfig sqlResultSetMappingConfig
      = _annotationCfg.getSqlResultSetMappingConfig();

    SqlResultSetMappings sqlResultSetMappingsAnn
      = (SqlResultSetMappings) type.getAnnotation(SqlResultSetMappings.class);

    if ((sqlResultSetMappingAnn == null) && (sqlResultSetMappingsAnn == null))
      return;

    SqlResultSetMapping sqlResultSetMappingArray[];

    if ((sqlResultSetMappingAnn != null) && (sqlResultSetMappingsAnn != null)) {
      throw new ConfigException(L.l("{0} may not have both @SqlResultSetMapping and @SqlResultSetMappings",
                                    typeName));
    }
    else if (sqlResultSetMappingsAnn != null) {
      sqlResultSetMappingArray = sqlResultSetMappingsAnn.value();
    }
    else {
      sqlResultSetMappingArray = new SqlResultSetMapping[] { sqlResultSetMappingAnn };
    }

    if (sqlResultSetMappingConfig != null) {
      _persistenceUnit.addSqlResultSetMapping(sqlResultSetMappingConfig.getName(),
                                              sqlResultSetMappingConfig);
      return;
    }

    for (int i=0; i < sqlResultSetMappingArray.length; i++) {
      sqlResultSetMappingAnn = sqlResultSetMappingArray[i];

      String name = sqlResultSetMappingAnn.name();
      EntityResult entities[] = sqlResultSetMappingAnn.entities();
      ColumnResult columns[] = sqlResultSetMappingAnn.columns();

      SqlResultSetMappingCompletion completion
        = new SqlResultSetMappingCompletion(entityType, name,
                                            entities, columns);

      _depCompletions.add(completion);
    }
  }

  /**
   * Completion callback for sql result set mappings.
   */
  void addSqlResultSetMapping(String resultSetName,
                              EntityResult entities[],
                              ColumnResult columns[])
    throws ConfigException
  {
    // jpa/0y1-

    SqlResultSetMappingConfig sqlResultSetMapping
      = new SqlResultSetMappingConfig();

    // Adds @EntityResult.
    for (int i=0; i < entities.length; i++) {
      EntityResult entityResult = entities[i];

      String className = entityResult.entityClass().getName();

      EntityType resultType = _persistenceUnit.getEntityType(className);

      if (resultType == null)
        throw new ConfigException(L.l("entityClass '{0}' is not an @Entity bean for @SqlResultSetMapping '{1}'. The entityClass of an @EntityResult must be an @Entity bean.",
                                      className,
                                      resultSetName));

      EntityResultConfig entityResultConfig = new EntityResultConfig();

      entityResultConfig.setEntityClass(className);

      // @FieldResult annotations.
      FieldResult fields[] = entityResult.fields();

      for (int j=0; j < fields.length; j++) {
        FieldResult fieldResult = fields[j];

        String fieldName = fieldResult.name();

        AmberField field = resultType.getField(fieldName);

        if (field == null)
          throw new ConfigException(L.l("@FieldResult with field name '{0}' is not a field for @EntityResult bean '{1}' in @SqlResultSetMapping '{2}'",
                                        fieldName,
                                        className,
                                        resultSetName));

        String columnName = fieldResult.column();

        if (columnName == null || columnName.length() == 0)
          throw new ConfigException(L.l("@FieldResult must have a column name defined and it must not be empty for '{0}' in @EntityResult '{1}' @SqlResultSetMapping '{2}'",
                                        fieldName,
                                        className,
                                        resultSetName));

        FieldResultConfig fieldResultConfig = new FieldResultConfig();

        fieldResultConfig.setName(fieldName);
        fieldResultConfig.setColumn(columnName);

        entityResultConfig.addFieldResult(fieldResultConfig);
      }

      sqlResultSetMapping.addEntityResult(entityResultConfig);
    }

    // Adds @ColumnResult.
    for (int i=0; i < columns.length; i++) {
      ColumnResult columnResult = columns[i];

      String columnName = columnResult.name();

      if (columnName == null || columnName.length() == 0)
        throw new ConfigException(L.l("@ColumnResult must have a column name defined and it must not be empty in @SqlResultSetMapping '{0}'",
                                      resultSetName));

      ColumnResultConfig columnResultConfig = new ColumnResultConfig();

      columnResultConfig.setName(columnName);

      sqlResultSetMapping.addColumnResult(columnResultConfig);
    }

    // Adds a global sql result set mapping to the persistence unit.
    _persistenceUnit.addSqlResultSetMapping(resultSetName,
                                            sqlResultSetMapping);
  }

  /**
   * Completes all partial bean introspection.
   */
  public void configureLinks()
    throws ConfigException
  {
    RuntimeException exn = null;

    while (_linkCompletions.size() > 0) {
      Completion completion = _linkCompletions.remove(0);

      try {
        completion.complete();
      } catch (Exception e) {
        if (e instanceof ConfigException) {
          log.warning(e.getMessage());
          log.log(Level.FINEST, e.toString(), e);
        }
        else
          log.log(Level.WARNING, e.toString(), e);

        completion.getRelatedType().setConfigException(e);

        if (exn == null)
          exn = ConfigException.create(e);
      }
    }

    if (exn != null)
      throw exn;
  }

  /**
   * Completes all partial bean introspection.
   */
  public void configureDependencies()
    throws ConfigException
  {
    RuntimeException exn = null;

    while (_depCompletions.size() > 0) {
      Completion completion = _depCompletions.remove(0);

      try {
        completion.complete();
      } catch (Exception e) {
        if (e instanceof ConfigException) {
          log.warning(e.getMessage());
          log.log(Level.FINEST, e.toString(), e);
        }
        else
          log.log(Level.WARNING, e.toString(), e);

        completion.getRelatedType().setConfigException(e);

        if (exn == null)
          exn = ConfigException.create(e);
      }
    }

    if (exn != null)
      throw exn;
  }

  /**
   * Introspects the fields.
   */
  void introspectIdMethod(AmberPersistenceUnit persistenceUnit,
                          EntityType entityType,
                          EntityType parentType,
                          Class type,
                          Class idClass,
                          MappedSuperclassConfig config)
    throws ConfigException, SQLException
  {
    ArrayList<IdField> keys = new ArrayList<IdField>();

    IdField idField = null;

    AttributesConfig attributesConfig = null;

    if (config != null)
      attributesConfig = config.getAttributes();

    for (Method method : type.getDeclaredMethods()) {
      String methodName = method.getName();
      Class []paramTypes = method.getParameterTypes();

      if (method.getDeclaringClass().equals(Object.class))
        continue;

      if (! methodName.startsWith("get") || paramTypes.length != 0) {
        continue;
      }

      String fieldName = toFieldName(methodName.substring(3));

      if (containsFieldOrCompletion(parentType, fieldName))
        continue;

      getInternalIdConfig(type, method, fieldName, _annotationCfg);
      Annotation id = _annotationCfg.getAnnotation();
      IdConfig idConfig = _annotationCfg.getIdConfig();

      if (! _annotationCfg.isNull()) {
        idField = introspectId(persistenceUnit,
                               entityType,
                               method,
                               fieldName,
                               method.getReturnType(),
                               idConfig);

        if (idField != null)
          keys.add(idField);
      }
      else {
        getInternalEmbeddedIdConfig(type, method, fieldName, _annotationCfg);
        Annotation embeddedId = _annotationCfg.getAnnotation();
        EmbeddedIdConfig embeddedIdConfig = _annotationCfg.getEmbeddedIdConfig();

        if (! _annotationCfg.isNull()) {
          idField = introspectEmbeddedId(persistenceUnit,
                                         entityType,
                                         method,
                                         fieldName,
                                         method.getReturnType());
          break;
        }
        else {
          continue;
        }
      }
    }

    if (keys.size() == 0) {
      if (idField != null) {
        // @EmbeddedId was used.
        com.caucho.amber.field.EmbeddedId id
          = new com.caucho.amber.field.EmbeddedId(entityType, (EmbeddedIdField) idField);

        entityType.setId(id);
      }
    }
    else if (keys.size() == 1) {
      entityType.setId(new com.caucho.amber.field.Id(entityType, keys));
    }
    else if (idClass == null) {
      throw new ConfigException(L.l("{0} has multiple @Id methods, but no @IdClass.  Compound primary keys require either an @IdClass or exactly one @EmbeddedId field or property.",
                                    entityType.getName()));
    }
    else {
      CompositeId id = new CompositeId(entityType, keys);
      id.setKeyClass(idClass);

      entityType.setId(id);
    }
  }

  /**
   * Introspects the fields.
   */
  void introspectIdField(AmberPersistenceUnit persistenceUnit,
                         EntityType entityType,
                         EntityType parentType,
                         Class type,
                         Class idClass,
                         MappedSuperclassConfig config)
    throws ConfigException, SQLException
  {
    ArrayList<IdField> keys = new ArrayList<IdField>();

    AttributesConfig attributesConfig = null;

    if (config != null)
      attributesConfig = config.getAttributes();

    for (Field field : type.getDeclaredFields()) {
      String fieldName = field.getName();

      if (containsFieldOrCompletion(parentType, fieldName))
        continue;

      getInternalIdConfig(type, field, fieldName, _annotationCfg);
      Annotation id = _annotationCfg.getAnnotation();
      IdConfig idConfig = _annotationCfg.getIdConfig();

      if (_annotationCfg.isNull()) {
        getInternalEmbeddedIdConfig(type, field, fieldName, _annotationCfg);
        Annotation embeddedId = _annotationCfg.getAnnotation();
        EmbeddedIdConfig embeddedIdConfig = _annotationCfg.getEmbeddedIdConfig();

        if (_annotationCfg.isNull())
          continue;
      }

      IdField idField = introspectId(persistenceUnit,
                                     entityType,
                                     field,
                                     fieldName,
                                     field.getType(),
                                     idConfig);

      if (idField != null)
        keys.add(idField);
    }

    if (keys.size() == 0) {
    }
    else if (keys.size() == 1)
      entityType.setId(new com.caucho.amber.field.Id(entityType, keys));
    else if (idClass == null) {
      throw new ConfigException(L.l("{0} has multiple @Id fields, but no @IdClass.  Compound primary keys require an @IdClass.",
                                    entityType.getName()));
    }
    else {
      CompositeId id = new CompositeId(entityType, keys);
      id.setKeyClass(idClass);

      entityType.setId(id);

      _configManager.introspect(idClass);
    }
  }

  /**
   * Check if it's field
   */
  boolean isField(Class type,
                  AbstractEnhancedConfig typeConfig,
                  boolean isEmbeddable)
    throws ConfigException
  {
    if (type == null)
      return false;

    if (typeConfig != null) {
      String access = typeConfig.getAccess();

      if (access != null)
        return access.equals("FIELD");

      Class parentClass = type.getSuperclass();

      if (parentClass == null)
        return false;
      else {
        getInternalEntityConfig(parentClass, _annotationCfg);
        EntityConfig superEntityConfig = _annotationCfg.getEntityConfig();

        if (superEntityConfig == null)
          return false;

        return isField(parentClass, superEntityConfig, false);
      }
    }

    for (Field field : type.getDeclaredFields()) {
      for (Class annType : _annTypes) {
        if (field.getAnnotation(annType) != null) {
          return true;
        }
      }
    }

    return isField(type.getSuperclass(), null, false);
  }

  private IdField introspectId(AmberPersistenceUnit persistenceUnit,
                               EntityType entityType,
                               AccessibleObject field,
                               String fieldName,
                               Class fieldType,
                               IdConfig idConfig)
    throws ConfigException, SQLException
  {
    javax.persistence.Id id = field.getAnnotation(javax.persistence.Id.class);
    Column column = field.getAnnotation(javax.persistence.Column.class);

    ColumnConfig columnConfig = null;
    GeneratedValueConfig generatedValueConfig = null;

    if (idConfig != null) {
      columnConfig = idConfig.getColumn();
      generatedValueConfig = idConfig.getGeneratedValue();
    }

    GeneratedValue gen = field.getAnnotation(GeneratedValue.class);

    AmberType amberType = persistenceUnit.createType(fieldType);

    KeyPropertyField idField;

    AmberColumn keyColumn = null;
    keyColumn = createColumn(entityType,
                             field,
                             fieldName,
                             column,
                             amberType,
                             columnConfig);


    if (entityType.getTable() != null) {
      idField = new KeyPropertyField(entityType, fieldName, keyColumn);
    }
    else {
      idField = new KeyPropertyField(entityType, fieldName, keyColumn);
      return idField;
    }

    if (gen == null) {
    }
    else {
      JdbcMetaData metaData = null;

      /* XXX: validation needs to occur later
      try {
        metaData = persistenceUnit.getMetaData();
      } catch (ConfigException e) {
        throw e;
      } catch (Exception e) {
        throw new ConfigException(L.l("Unable to get meta data for database. Meta data is needed for generated values."), e);
      }
      */

      if (GenerationType.IDENTITY.equals(gen.strategy())) {
        /* XXX: validation later
        if (! metaData.supportsIdentity())
          throw new ConfigException(L.l("'{0}' does not support identity.",
                                        metaData.getDatabaseName()));
        */

        keyColumn.setGeneratorType("identity");
        idField.setGenerator("identity");
      }
      else if (GenerationType.SEQUENCE.equals(gen.strategy())) {
        /* XXX: validation later
        if (! metaData.supportsSequences())
          throw new ConfigException(L.l("'{0}' does not support sequence.",
                                        metaData.getDatabaseName()));
        */

        addSequenceIdGenerator(persistenceUnit, idField, gen);
      }
      else if (GenerationType.TABLE.equals(gen.strategy())) {
        addTableIdGenerator(persistenceUnit, idField, id);
      }
      else if (GenerationType.AUTO.equals(gen.strategy())) {
        keyColumn.setGeneratorType("auto");
        idField.setGenerator("auto");

        /* XXX: validation later
        if (metaData.supportsIdentity()) {
          keyColumn.setGeneratorType("identity");
          idField.setGenerator("identity");
        }
        else if (metaData.supportsSequences()) {
          addSequenceIdGenerator(persistenceUnit, idField, gen);
        }
        else {
          addTableIdGenerator(persistenceUnit, idField, id);
        }
        */
      }
    }

    return idField;
  }

  private IdField introspectEmbeddedId(AmberPersistenceUnit persistenceUnit,
                                       EntityType ownerType,
                                       AccessibleObject field,
                                       String fieldName,
                                       Class fieldType)
    throws ConfigException, SQLException
  {
    IdField idField;

    EmbeddableType embeddableType
      = (EmbeddableType) _configManager.introspect(fieldType);

    if (embeddableType == null)
      throw new IllegalStateException("" + fieldType + " is an unsupported embeddable type");

    idField = new EmbeddedIdField(ownerType, embeddableType, fieldName);

    return idField;
  }

  void addSequenceIdGenerator(AmberPersistenceUnit persistenceUnit,
                              KeyPropertyField idField,
                              GeneratedValue genAnn)
    throws ConfigException
  {
    idField.setGenerator("sequence");
    idField.getColumn().setGeneratorType("sequence");

    String name = genAnn.generator();

    if (name == null || "".equals(name))
      name = idField.getEntitySourceType().getTable().getName() + "_cseq";

    IdGenerator gen = persistenceUnit.createSequenceGenerator(name, 1);

    idField.getEntitySourceType().setGenerator(idField.getName(), gen);
  }

  void addTableIdGenerator(AmberPersistenceUnit persistenceUnit,
                           KeyPropertyField idField,
                           javax.persistence.Id idAnn)
    throws ConfigException
  {
    idField.setGenerator("table");
    idField.getColumn().setGeneratorType("table");

    String name = null;// XXX: idAnn.name();
    if (name == null || "".equals(name))
      name = "caucho";

    IdGenerator gen = persistenceUnit.getTableGenerator(name);

    if (gen == null) {
      String genName = "GEN_TABLE";

      GeneratorTableType genTable;
      genTable = persistenceUnit.createGeneratorTable(genName);

      gen = genTable.createGenerator(name);

      // jpa/0g60
      genTable.init();

      persistenceUnit.putTableGenerator(name, gen);
    }

    idField.getEntitySourceType().setGenerator(idField.getName(), gen);
  }

  /**
   * Links a secondary table.
   */
  void linkSecondaryTable(AmberTable primaryTable,
                          AmberTable secondaryTable,
                          PrimaryKeyJoinColumn []joinColumnsAnn)
    throws ConfigException
  {
    ArrayList<ForeignColumn> linkColumns = new ArrayList<ForeignColumn>();
    for (AmberColumn column : primaryTable.getIdColumns()) {
      ForeignColumn linkColumn;

      PrimaryKeyJoinColumn joinAnn
        = getJoinColumn(joinColumnsAnn, column.getName());
      String name;

      if (joinAnn == null)
        name = column.getName();
      else
        name = joinAnn.name();

      linkColumn = secondaryTable.createForeignColumn(name, column);
      linkColumn.setPrimaryKey(true);

      secondaryTable.addIdColumn(linkColumn);

      linkColumns.add(linkColumn);
    }

    LinkColumns link = new LinkColumns(secondaryTable,
                                       primaryTable,
                                       linkColumns);

    link.setSourceCascadeDelete(true);

    secondaryTable.setDependentIdLink(link);
  }

  /**
   * Links a secondary table.
   */
  void linkInheritanceTable(AmberTable primaryTable,
                            AmberTable secondaryTable,
                            PrimaryKeyJoinColumn joinAnn,
                            PrimaryKeyJoinColumnConfig pkJoinColumnCfg)
    throws ConfigException
  {
    PrimaryKeyJoinColumn joinAnns[] = null;

    if (joinAnn != null)
      joinAnns = new PrimaryKeyJoinColumn[] { joinAnn };

    linkInheritanceTable(primaryTable,
                         secondaryTable,
                         joinAnns,
                         pkJoinColumnCfg);
  }

  /**
   * Links a secondary table.
   */
  void linkInheritanceTable(AmberTable primaryTable,
                            AmberTable secondaryTable,
                            PrimaryKeyJoinColumn []joinColumnsAnn,
                            PrimaryKeyJoinColumnConfig pkJoinColumnCfg)
    throws ConfigException
  {
    ArrayList<ForeignColumn> linkColumns = new ArrayList<ForeignColumn>();
    for (AmberColumn column : primaryTable.getIdColumns()) {
      ForeignColumn linkColumn;

      String name;

      if (joinColumnsAnn == null) {

        if (pkJoinColumnCfg == null)
          name = column.getName();
        else
          name = pkJoinColumnCfg.getName();
      }
      else {
        PrimaryKeyJoinColumn join;

        join = getJoinColumn(joinColumnsAnn, column.getName());

        if (join == null)
          name = column.getName();
        else
          name = join.name();
      }

      linkColumn = secondaryTable.createForeignColumn(name, column);
      linkColumn.setPrimaryKey(true);

      secondaryTable.addIdColumn(linkColumn);

      linkColumns.add(linkColumn);
    }

    LinkColumns link = new LinkColumns(secondaryTable,
                                       primaryTable,
                                       linkColumns);

    link.setSourceCascadeDelete(true);

    secondaryTable.setDependentIdLink(link);

    // jpa/0l48
    //    link = new LinkColumns(primaryTable,
    //                           secondaryTable,
    //                           linkColumns);
    //
    //    link.setSourceCascadeDelete(true);
    //
    //    primaryTable.setDependentIdLink(link);
  }

  /**
   * Introspects the methods.
   */
  void introspectMethods(AmberPersistenceUnit persistenceUnit,
                         AmberBeanType entityType,
                         AmberBeanType parentType,
                         Class type,
                         AbstractEnhancedConfig typeConfig)
    throws ConfigException
  {
    for (Method method : type.getDeclaredMethods()) {
      String methodName = method.getName();
      Class []paramTypes = method.getParameterTypes();

      if (method.getDeclaringClass().equals(Object.class))
        continue;

      // jpa/0r38
      // Callbacks are introspected in the main introspect() block.
      // introspectCallbacks(entityType, method);

      String propName;

      if (paramTypes.length != 0) {
        validateNonGetter(method);
        continue;
      }
      else if (methodName.startsWith("get")) {
        propName = methodName.substring(3);
      }
      else if (methodName.startsWith("is")
               && (method.getReturnType().equals(boolean.class)
                   || method.getReturnType().equals(Boolean.class))) {
        propName = methodName.substring(2);
      }
      else {
        validateNonGetter(method);
        continue;
      }

      getInternalVersionConfig(type, method, propName, _annotationCfg);
      Annotation versionAnn = _annotationCfg.getAnnotation();
      VersionConfig versionConfig = _annotationCfg.getVersionConfig();

      if (! _annotationCfg.isNull()) {
        validateNonGetter(method);
      }
      else {
        Method setter = null;
        
        try {
          setter = getMethod(type,
                             "set" + propName,
                             new Class[] { method.getReturnType() });
        } catch (Exception e) {
          log.log(Level.FINEST, e.toString(), e);
        }
        
        if (Modifier.isPrivate(method.getModifiers())
            || setter == null
            || Modifier.isPrivate(setter.getModifiers())) {
          Annotation ann = isAnnotatedMethod(method);

          if (ann == null) {
            if (setter != null)
              ann = isAnnotatedMethod(setter);
          }
          else if (ann instanceof Transient)
            continue;

          if (ann != null) {
            throw error(method, L.l("'{0}' is not a valid annotation for {1}.  Only public persistent property getters with matching setters may have property annotations.",
                                          ann.getClass(), getFullName(method)));
          }

          continue;
        }

        // ejb/0g03 for private
        if (Modifier.isStatic(method.getModifiers())) { // || ! method.isPublic()) {
          validateNonGetter(method);
          continue;
        }
      }

      String fieldName = toFieldName(propName);

      if (containsFieldOrCompletion(parentType, fieldName))
        continue;

      Class fieldType = method.getReturnType();

      introspectField(persistenceUnit, entityType, method,
                      fieldName, fieldType, typeConfig);
    }
  }

  private Method getMethod(Class cl, String name, Class []param)
  {
    if (cl == null)
      return null;

    loop:
    for (Method method : cl.getDeclaredMethods()) {
      if (! method.getName().equals(name))
        continue;

      Class []types = method.getParameterTypes();

      if (types.length != param.length)
        continue;
      
      for (int i = 0; i < types.length; i++) {
        if (! param[i].equals(types[i]))
          continue loop;
      }

      return method;
    }

    return getMethod(cl.getSuperclass(), name, param);
  }

  /**
   * Introspects the fields.
   */
  void introspectFields(AmberPersistenceUnit persistenceUnit,
                        AmberBeanType entityType,
                        AmberBeanType parentType,
                        Class type,
                        AbstractEnhancedConfig typeConfig,
                        boolean isEmbeddable)
    throws ConfigException
  {
    if (entityType.isEntity() && ((EntityType) entityType).getId() == null)
      throw new ConfigException(L.l("{0} has no key", entityType));

    for (Field field : type.getDeclaredFields()) {
      String fieldName = field.getName();

      if (containsFieldOrCompletion(parentType, fieldName)) {
        continue;
      }

      if (Modifier.isStatic(field.getModifiers())
          || Modifier.isTransient(field.getModifiers()))
        continue;

      Class fieldType = field.getType();
      introspectField(persistenceUnit, entityType, field,
                      fieldName, fieldType, typeConfig);
    }
  }

  void introspectField(AmberPersistenceUnit persistenceUnit,
                       AmberBeanType sourceType,
                       AccessibleObject field,
                       String fieldName,
                       Class fieldType,
                       AbstractEnhancedConfig typeConfig)
    throws ConfigException
  {
    EmbeddableConfig embeddableConfig = null;
    MappedSuperclassConfig mappedSuperOrEntityConfig = null;

    if (typeConfig instanceof EmbeddableConfig)
      embeddableConfig = (EmbeddableConfig) typeConfig;
    else if (typeConfig instanceof MappedSuperclassConfig)
      mappedSuperOrEntityConfig = (MappedSuperclassConfig) typeConfig;

    // jpa/0r37: interface fields must not be considered.

    Class jClass;
    
    if (field instanceof Field)
      jClass = ((Field) field).getDeclaringClass();
    else
      jClass = ((Method) field).getDeclaringClass();

    if (jClass.isInterface())
      return;

    // jpa/0r37: fields declared in non-entity superclasses
    // must not be considered.

    AmberBeanType declaringType;

    declaringType = _persistenceUnit.getEntityType(jClass.getName());

    if (declaringType == null)
      declaringType = _persistenceUnit.getEmbeddable(jClass.getName());

    if (declaringType == null)
      declaringType = _persistenceUnit.getMappedSuperclass(jClass.getName());

    if (declaringType == null)
      return;

    AttributesConfig attributesConfig = null;
    IdConfig idConfig = null;
    BasicConfig basicConfig = null;
    OneToOneConfig oneToOneConfig = null;
    OneToManyConfig oneToManyConfig = null;
    ManyToOneConfig manyToOneConfig = null;
    ManyToManyConfig manyToManyConfig = null;
    VersionConfig versionConfig = null;
    ElementCollectionConfig elementCollectionConfig = null;

    if (mappedSuperOrEntityConfig != null) {
      attributesConfig = mappedSuperOrEntityConfig.getAttributes();

      if (attributesConfig != null) {
        idConfig = attributesConfig.getId(fieldName);

        basicConfig = attributesConfig.getBasic(fieldName);

        oneToOneConfig = attributesConfig.getOneToOne(fieldName);

        oneToManyConfig = attributesConfig.getOneToMany(fieldName);

        elementCollectionConfig = null; // attributesConfig.getOneToMany(fieldName);

        manyToOneConfig = attributesConfig.getManyToOne(fieldName);

        manyToManyConfig = attributesConfig.getManyToMany(fieldName);

        versionConfig = attributesConfig.getVersion(fieldName);
      }
    }

    if (idConfig != null
        || field.isAnnotationPresent(javax.persistence.Id.class)) {
      validateAnnotations(field, fieldName, "@Id", _idAnnotations);

      if (! _idTypes.contains(fieldType.getName())) {
        throw error(field, L.l("{0} is an invalid @Id type for {1}.",
                               fieldType.getName(), fieldName));
      }
    }
    else if (basicConfig != null
             || field.isAnnotationPresent(javax.persistence.Basic.class)) {
      validateAnnotations(field, fieldName, "@Basic", _basicAnnotations);

      BasicConfig basic
        = new BasicConfig(this, sourceType, field, fieldName, fieldType);
      
      basic.complete();
    }
    else if ((versionConfig != null)
             || field.isAnnotationPresent(javax.persistence.Version.class)) {
      validateAnnotations(field, fieldName, "@Version", _versionAnnotations);

      addVersion((EntityType) sourceType, field,
                 fieldName, fieldType, versionConfig);
    }
    else if (field.isAnnotationPresent(javax.persistence.ManyToOne.class)) {
      validateAnnotations(field, fieldName, "@ManyToOne", _manyToOneAnnotations);

      ManyToOne ann = field.getAnnotation(ManyToOne.class);

      Class targetEntity = null;

      if (ann != null)
        targetEntity = ann.targetEntity();
      else {
        targetEntity = manyToOneConfig.getTargetEntity();
     }

      if (targetEntity == null ||
          targetEntity.getName().equals("void")) {
        targetEntity = fieldType;
      }

      getInternalEntityConfig(targetEntity, _annotationCfg);
      Annotation targetEntityAnn = _annotationCfg.getAnnotation();
      EntityConfig targetEntityConfig = _annotationCfg.getEntityConfig();

      if (_annotationCfg.isNull()) {
        throw error(field, L.l("'{0}' is an illegal targetEntity for {1}.  @ManyToOne relations must target a valid @Entity.",
                               targetEntity.getName(), fieldName));
      }

      if (! fieldType.isAssignableFrom(targetEntity)) {
        throw error(field, L.l("'{0}' is an illegal targetEntity for {1}.  @ManyToOne targetEntity must be assignable to the field type '{2}'.",
                               targetEntity.getName(),
                               fieldName,
                               fieldType.getName()));
      }

      EntityType entityType = (EntityType) sourceType;

      entityType.setHasDependent(true);

      _linkCompletions.add(new ManyToOneConfig(this,
                                               entityType,
                                               field,
                                               fieldName,
                                               fieldType));
    }
    else if (oneToManyConfig != null
             || field.isAnnotationPresent(javax.persistence.OneToMany.class)) {
      validateAnnotations(field, fieldName, "@OneToMany", _oneToManyAnnotations);

      if (field.isAnnotationPresent(javax.persistence.MapKey.class)) {
        if (!fieldType.getName().equals("java.util.Map")) {
          throw error(field, L.l("'{0}' is an illegal @OneToMany/@MapKey type for {1}. @MapKey must be a java.util.Map",
                                 fieldType.getName(),
                                 fieldName));
        }
      }
      else if (! _oneToManyTypes.contains(fieldType.getName())) {
        throw error(field, L.l("'{0}' is an illegal @OneToMany type for {1}.  @OneToMany must be a java.util.Collection, java.util.List or java.util.Map",
                               fieldType.getName(),
                               fieldName));
      }

      EntityType entityType = (EntityType) sourceType;

      _depCompletions.add(new OneToManyConfig(this,
                                              entityType,
                                              field,
                                              fieldName,
                                              fieldType));
    }
    else if ((oneToOneConfig != null)
             || field.isAnnotationPresent(javax.persistence.OneToOne.class)) {
      validateAnnotations(field, fieldName, "@OneToOne", _oneToOneAnnotations);

      EntityType entityType = (EntityType) sourceType;

      OneToOneConfig oneToOne
        = new OneToOneConfig(this, entityType, field, fieldName, fieldType);
      
      if (oneToOne.isOwningSide())
        _linkCompletions.add(oneToOne);
      else {
        _depCompletions.add(0, oneToOne);
        entityType.setHasDependent(true);
      }
    }
    else if ((manyToManyConfig != null)
             || field.isAnnotationPresent(javax.persistence.ManyToMany.class)) {

      if (field.isAnnotationPresent(javax.persistence.MapKey.class)) {
        if (! fieldType.getName().equals("java.util.Map")) {
          throw error(field, L.l("'{0}' is an illegal @ManyToMany/@MapKey type for {1}. @MapKey must be a java.util.Map",
                                 fieldType.getName(),
                                 fieldName));
        }
      }

      ManyToManyConfig manyToMany
        = new ManyToManyConfig(this, (EntityType) sourceType, field, fieldName, fieldType);

      if (manyToMany.isOwningSide())
        _linkCompletions.add(manyToMany);
      else
        _depCompletions.add(manyToMany);
    }
    else if (elementCollectionConfig != null
             || field.isAnnotationPresent(javax.persistence.ElementCollection.class)) {
      validateAnnotations(field, fieldName, "@ElementCollection", _elementCollectionAnnotations);
      if (! _elementCollectionTypes.contains(fieldType.getName())) {
        throw error(field, L.l("'{0}' is an illegal @ElementCollection type for {1}.  @ElementCollection must be a java.util.Collection, java.util.List or java.util.Map",
                               fieldType.getName(),
                               fieldName));
      }

      EntityType entityType = (EntityType) sourceType;

      ElementCollectionConfig comp
        = new ElementCollectionConfig(entityType,
                                      field,
                                      fieldName,
                                      fieldType);

      _depCompletions.add(comp);
    }
    else if (field.isAnnotationPresent(javax.persistence.Embedded.class)) {
      validateAnnotations(field, fieldName, "@Embedded", _embeddedAnnotations);

      EntityType entityType = (EntityType) sourceType;

      entityType.setHasDependent(true);

      _depCompletions.add(new EmbeddedCompletion(entityType,
                                                 field,
                                                 fieldName,
                                                 fieldType,
                                                 false));
    }
    else if (field.isAnnotationPresent(javax.persistence.EmbeddedId.class)) {
      validateAnnotations(field, fieldName, "@EmbeddedId", _embeddedIdAnnotations);

      _depCompletions.add(new EmbeddedCompletion((EntityType) sourceType,
                                                 field,
                                                 fieldName,
                                                 fieldType,
                                                 true));
    }
    else if (field.isAnnotationPresent(javax.persistence.Transient.class)) {
    }
    else {
      BasicConfig basic
        = new BasicConfig(this, sourceType, field, fieldName, fieldType);
      
      basic.complete();
    }
  }

  void addVersion(EntityType sourceType,
                  AccessibleObject field,
                  String fieldName,
                  Class fieldType,
                  VersionConfig versionConfig)
    throws ConfigException
  {
    AmberPersistenceUnit persistenceUnit = sourceType.getPersistenceUnit();

    Column columnAnn = field.getAnnotation(Column.class);

    ColumnConfig columnConfig = null;

    if (versionConfig != null)
      columnConfig = versionConfig.getColumn();

    if (! _versionTypes.contains(fieldType.getName())) {
      throw error(field, L.l("{0} is an invalid @Version type for {1}.",
                             fieldType.getName(), fieldName));
    }

    AmberType amberType = persistenceUnit.createType(fieldType);

    AmberColumn fieldColumn = createColumn(sourceType, field, fieldName,
                                      columnAnn, amberType, columnConfig);

    VersionField version = new VersionField(sourceType, fieldName);
    version.setColumn(fieldColumn);

    sourceType.setVersionField(version);
  }

  private AmberColumn createColumn(AmberBeanType beanType,
                              AccessibleObject field,
                              String fieldName,
                              Column columnAnn,
                              AmberType amberType,
                              ColumnConfig columnConfig)
    throws ConfigException
  {
    EntityType entityType = null;

    if (beanType instanceof EntityType)
      entityType = (EntityType) beanType;
    
    String name;

    if (columnAnn != null && ! columnAnn.name().equals(""))
      name = (String) columnAnn.name();
    else if (columnConfig != null && ! columnConfig.getName().equals(""))
      name = columnConfig.getName();
    else
      name = toSqlName(fieldName);

    AmberColumn column = null;

    if (entityType == null) { // embeddable
      column = new AmberColumn(null, name, amberType);
    }
    else if (columnAnn != null && ! columnAnn.table().equals("")) {
      String tableName = columnAnn.table();
      AmberTable table;

      table = entityType.getSecondaryTable(tableName);

      if (table == null)
        throw error(field, L.l("{0} @Column(table='{1}') is an unknown secondary table.",
                               fieldName,
                               tableName));

      column = table.createColumn(name, amberType);
    }
    else if (entityType.getTable() != null)
      column = entityType.getTable().createColumn(name, amberType);
    else { // jpa/0ge2: MappedSuperclassType
      column = new AmberColumn(null, name, amberType);
    }

    if (column != null && columnAnn != null) {
      // primaryKey = column.primaryKey();
      column.setUnique(columnAnn.unique());
      column.setNotNull(! columnAnn.nullable());
      //insertable = column.insertable();
      //updateable = column.updatable();
      if (! "".equals(columnAnn.columnDefinition()))
        column.setSQLType(columnAnn.columnDefinition());
      column.setLength(columnAnn.length());
      int precision = columnAnn.precision();
      if (precision < 0) {
        throw error(field, L.l("{0} @Column precision cannot be less than 0.",
                               fieldName));
      }

      int scale = columnAnn.scale();
      if (scale < 0) {
        throw error(field, L.l("{0} @Column scale cannot be less than 0.",
                               fieldName));
      }

      // this test implicitly works for case where
      // precision is not set explicitly (ie: set to 0 by default)
      // and scale is set
      if (scale > precision) {
        throw error(field, L.l("{0} @Column scale cannot be greater than precision. Must set precision to a non-zero value before setting scale.",
                               fieldName));
      }

      if (precision > 0) {
        column.setPrecision(precision);
        column.setScale(scale);
      }
    }

    return column;
  }

  public static JoinColumn getJoinColumn(JoinColumns joinColumns,
                                         String keyName)
  {
    if (joinColumns == null)
      return null;

    return getJoinColumn(joinColumns.value(), keyName);
  }

  private AmberColumn findColumn(ArrayList<AmberColumn> columns, String ref)
  {
    if (((ref == null) || ref.equals("")) && columns.size() == 1)
      return columns.get(0);

    for (AmberColumn column : columns) {
      if (column.getName().equals(ref))
        return column;
    }

    return null;
  }

  public static JoinColumn getJoinColumn(JoinColumn []columnsAnn,
                                          String keyName)
  {
    if (columnsAnn == null || columnsAnn.length == 0)
      return null;

    for (int i = 0; i < columnsAnn.length; i++) {
      String ref = columnsAnn[i].referencedColumnName();

      if (ref.equals("") || ref.equals(keyName))
        return columnsAnn[i];
    }

    return null;
  }

  public static PrimaryKeyJoinColumn getJoinColumn(PrimaryKeyJoinColumn []columnsAnn,
                                                   String keyName)
  {
    if (columnsAnn == null || columnsAnn.length == 0)
      return null;

    for (int i = 0; i < columnsAnn.length; i++) {
      String ref = columnsAnn[i].referencedColumnName();

      if (ref.equals("") || ref.equals(keyName))
        return columnsAnn[i];
    }

    return null;
  }

  /**
   * completes for dependent
   */
  class EmbeddedCompletion extends CompletionImpl {
    private AccessibleObject _field;
    private String _fieldName;
    private Class _fieldType;
    // The same completion is used for both:
    // @Embedded or @EmbeddedId
    private boolean _embeddedId;

    EmbeddedCompletion(EntityType type,
                       AccessibleObject field,
                       String fieldName,
                       Class fieldType,
                       boolean embeddedId)
    {
      super(BaseConfigIntrospector.this, type, fieldName);

      _field = field;
      _fieldName = fieldName;
      _fieldType = fieldType;
      _embeddedId = embeddedId;
    }

    @Override
    public void complete()
      throws ConfigException
    {
      getInternalAttributeOverrideConfig(_entityType.getBeanClass(),
                                         _annotationCfg);
      AttributeOverride attributeOverrideAnn = (AttributeOverride) _annotationCfg.getAnnotation();
      AttributeOverrideConfig attributeOverrideConfig = _annotationCfg.getAttributeOverrideConfig();

      boolean hasAttributeOverride = ! _annotationCfg.isNull();

      AttributeOverrides attributeOverridesAnn = _field.getAnnotation(AttributeOverrides.class);

      boolean hasAttributeOverrides = (attributeOverridesAnn != null);

      if (hasAttributeOverride && hasAttributeOverrides) {
        throw error(_field, L.l("{0} may not have both @AttributeOverride and @AttributeOverrides",
                                _fieldName));
      }

      AttributeOverride attOverridesAnn[] = null;

      if (attributeOverrideAnn != null) {
        attOverridesAnn = new AttributeOverride[] { attributeOverrideAnn };
      }
      else if (attributeOverridesAnn != null) {
        attOverridesAnn = attributeOverridesAnn.value();
      }

      AmberPersistenceUnit persistenceUnit = _entityType.getPersistenceUnit();

      EmbeddableType type = persistenceUnit.createEmbeddable(_fieldType);

      EntityEmbeddedField embeddedField;

      if (_embeddedId) {
        embeddedField = _entityType.getId().getEmbeddedIdField();
      } else {
        embeddedField = new EntityEmbeddedField(_entityType, type, _fieldName);
      }

      // embeddedField.setEmbeddedId(_embeddedId);

      embeddedField.setLazy(false);

      _entityType.addField(embeddedField);

      // XXX: todo ...
      // validateAttributeOverrides(_field, attributeOverridesAnn, type);

      AmberTable sourceTable = _entityType.getTable();

      HashMap<String, AmberColumn> embeddedColumns = new HashMap<String, AmberColumn>();
      HashMap<String, String> fieldNameByColumn = new HashMap<String, String>();

      for (EmbeddedSubField subField : embeddedField.getSubFields()) {
        String embeddedFieldName = subField.getName();

        String columnName = toSqlName(embeddedFieldName);
        boolean notNull = false;
        boolean unique = false;

        if (attOverridesAnn != null) {
          for (int j= 0; j < attOverridesAnn.length; j++) {

            if (embeddedFieldName.equals(attOverridesAnn[j].name())) {

              Column columnAnn = attOverridesAnn[j].column();

              if (columnAnn != null) {
                columnName = columnAnn.name();
                notNull = ! columnAnn.nullable();
                unique = columnAnn.unique();

                subField.getColumn().setName(columnName);
                subField.getColumn().setNotNull(notNull);
                subField.getColumn().setUnique(unique);
              }
            }
          }
        }

        /*
        AmberType amberType = _persistenceUnit.createType(fields.get(i).getJavaType().getName());

        AmberColumn column = sourceTable.createColumn(columnName, amberType);

        column.setNotNull(notNull);
        column.setUnique(unique);

        embeddedColumns.put(columnName, column);
        fieldNameByColumn.put(columnName, embeddedFieldName);
        */
      }

      /*
      embeddedField.setEmbeddedColumns(embeddedColumns);
      embeddedField.setFieldNameByColumn(fieldNameByColumn);
      */

      embeddedField.init();
    }
  }

  /**
   * completes for dependent
   */
  class SqlResultSetMappingCompletion extends CompletionImpl {
    private String _name;
    private EntityResult _entities[];
    private ColumnResult _columns[];

    SqlResultSetMappingCompletion(EntityType type,
                                  String name,
                                  EntityResult entities[],
                                  ColumnResult columns[])
    {
      super(BaseConfigIntrospector.this, type);

      _name = name;
      _entities = entities;
      _columns = columns;
    }

    @Override
    public void complete()
      throws ConfigException
    {
      addSqlResultSetMapping(_name,
                             _entities,
                             _columns);
    }
  }

  void getInternalEmbeddableConfig(Class type,
                                   AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, Embeddable.class);

    EmbeddableConfig embeddableConfig = null;

    if (_embeddableConfigMap != null)
      embeddableConfig = _embeddableConfigMap.get(type.getName());

    annotationCfg.setConfig(embeddableConfig);
  }

  void getInternalEntityConfig(Class type, AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, Entity.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    annotationCfg.setConfig(entityConfig);
  }

  void getInternalMappedSuperclassConfig(Class type,
                                         AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, MappedSuperclass.class);

    MappedSuperclassConfig mappedSuperConfig
      = getMappedSuperclassConfig(type.getName());

    annotationCfg.setConfig(mappedSuperConfig);
  }

  void getInternalEntityListenersConfig(Class type,
                                        AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, EntityListeners.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    annotationCfg.setConfig(entityConfig.getEntityListeners());
  }

  void getInternalExcludeDefaultListenersConfig(Class type,
                                                AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, ExcludeDefaultListeners.class);

    MappedSuperclassConfig entityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    if (entityConfig.getExcludeDefaultListeners())
      annotationCfg.setConfig(entityConfig.getExcludeDefaultListeners());
  }

  void getInternalExcludeSuperclassListenersConfig(Class type,
                                                   AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, ExcludeSuperclassListeners.class);

    MappedSuperclassConfig entityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    if (entityConfig.getExcludeSuperclassListeners())
      annotationCfg.setConfig(entityConfig.getExcludeSuperclassListeners());
  }

  void getInternalInheritanceConfig(Class type,
                                    AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, Inheritance.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null) {
      annotationCfg.setConfig(entityConfig.getInheritance());
    }
  }

  void getInternalNamedQueryConfig(Class type, AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, NamedQuery.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null) {
      annotationCfg.setConfig(entityConfig.getNamedQuery());
    }
  }

  void getInternalNamedNativeQueryConfig(Class type,
                                         AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, NamedNativeQuery.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null) {
      annotationCfg.setConfig(entityConfig.getNamedNativeQuery());
    }
  }

  void getInternalSqlResultSetMappingConfig(Class type,
                                            AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, SqlResultSetMapping.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null) {
      annotationCfg.setConfig(entityConfig.getSqlResultSetMapping());
    }
  }

  void getInternalTableConfig(Class type,
                              AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, javax.persistence.Table.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null) {
      annotationCfg.setConfig(entityConfig.getTable());
    }
  }

  void getInternalSecondaryTableConfig(Class type,
                                       AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, SecondaryTable.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null) {
      annotationCfg.setConfig(entityConfig.getSecondaryTable());
    }
  }

  void getInternalIdClassConfig(Class type,
                                AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, IdClass.class);

    MappedSuperclassConfig entityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    annotationCfg.setConfig(entityConfig.getIdClass());
  }

  void getInternalPrimaryKeyJoinColumnConfig(Class type,
                                             AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, PrimaryKeyJoinColumn.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null) {
      annotationCfg.setConfig(entityConfig.getPrimaryKeyJoinColumn());
    }
  }

  void getInternalDiscriminatorColumnConfig(Class type,
                                            AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, DiscriminatorColumn.class);

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null) {
      annotationCfg.setConfig(entityConfig.getDiscriminatorColumn());
    }
  }

  void getInternalOneToOneConfig(Class type,
                                 AccessibleObject field,
                                 String fieldName,
                                 AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(field, OneToOne.class);

    MappedSuperclassConfig entityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    AttributesConfig attributes = entityConfig.getAttributes();

    if (attributes != null) {
      OneToOneConfig oneToOne = attributes.getOneToOne(fieldName);

      annotationCfg.setConfig(oneToOne);
    }
  }

  void getInternalOneToManyConfig(Class type,
                                  AccessibleObject field,
                                  String fieldName,
                                  AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(field, OneToMany.class);

    MappedSuperclassConfig entityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    AttributesConfig attributes = entityConfig.getAttributes();

    if (attributes != null) {
      OneToManyConfig oneToMany = attributes.getOneToMany(fieldName);

      annotationCfg.setConfig(oneToMany);
    }
  }

  void getInternalManyToOneConfig(Class type,
                                  AccessibleObject field,
                                  String fieldName,
                                  AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(field, ManyToOne.class);

    MappedSuperclassConfig entityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    AttributesConfig attributes = entityConfig.getAttributes();

    if (attributes != null) {
      ManyToOneConfig manyToOne = attributes.getManyToOne(fieldName);

      annotationCfg.setConfig(manyToOne);
    }
  }

  void getInternalManyToManyConfig(Class type,
                                   AccessibleObject field,
                                   String fieldName,
                                   AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(field, ManyToMany.class);

    MappedSuperclassConfig entityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    AttributesConfig attributes = entityConfig.getAttributes();

    if (attributes != null) {
      ManyToManyConfig manyToMany = attributes.getManyToMany(fieldName);

      annotationCfg.setConfig(manyToMany);
    }
  }

  void getInternalIdConfig(Class type,
                           AccessibleObject method,
                           String fieldName,
                           AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(method, javax.persistence.Id.class);

    MappedSuperclassConfig mappedSuperclassOrEntityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (mappedSuperclassOrEntityConfig == null)
      return;

    AttributesConfig attributes
      = mappedSuperclassOrEntityConfig.getAttributes();

    if (attributes != null) {
      IdConfig id = attributes.getId(fieldName);

      annotationCfg.setConfig(id);
    }
  }

  void getInternalCallbackConfig(int callback,
                                 Class type,
                                 AccessibleObject method,
                                 String fieldName,
                                 AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(method, ListenerType.CALLBACK_CLASS[callback]);

    MappedSuperclassConfig entityConfig
      = getInternalMappedSuperclassOrEntityConfig(type.getName());

    if (entityConfig == null)
      return;

    AbstractListenerConfig callbackConfig;

    switch (callback) {
    case Listener.PRE_PERSIST:
      callbackConfig = entityConfig.getPrePersist();
      break;
    case Listener.POST_PERSIST:
      callbackConfig = entityConfig.getPostPersist();
      break;
    case Listener.PRE_REMOVE:
      callbackConfig = entityConfig.getPreRemove();
      break;
    case Listener.POST_REMOVE:
      callbackConfig = entityConfig.getPostRemove();
      break;
    case Listener.PRE_UPDATE:
      callbackConfig = entityConfig.getPreUpdate();
      break;
    case Listener.POST_UPDATE:
      callbackConfig = entityConfig.getPostUpdate();
      break;
    case Listener.POST_LOAD:
      callbackConfig = entityConfig.getPostLoad();
      break;
    default:
      return;
    }

    if (callbackConfig == null)
      return;

    if (callbackConfig.getMethodName().equals(((Method) method).getName()))
      annotationCfg.setConfig(callbackConfig);
  }

  void getInternalEmbeddedIdConfig(Class type,
                                   AccessibleObject method,
                                   String fieldName,
                                   AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(method, EmbeddedId.class);
  }

  void getInternalVersionConfig(Class type,
                                AccessibleObject method,
                                String fieldName,
                                AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(method, Version.class);
  }

  void getInternalJoinColumnConfig(Class type,
                                   AccessibleObject field,
                                   String fieldName,
                                   AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(field, JoinColumn.class);
  }

  void getInternalJoinTableConfig(Class type,
                                  AccessibleObject field,
                                  String fieldName,
                                  AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(field, JoinTable.class);
  }

  void getInternalMapKeyConfig(Class type,
                               AccessibleObject field,
                               String fieldName,
                               AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(field, MapKey.class);
  }

  void getInternalAttributeOverrideConfig(Class type,
                                          AnnotationConfig annotationCfg)
  {
    annotationCfg.reset(type, AttributeOverride.class);
  }

  private MappedSuperclassConfig getInternalMappedSuperclassOrEntityConfig(String name)
  {
    MappedSuperclassConfig mappedSuperclassConfig = null;

    mappedSuperclassConfig = getEntityConfig(name);

    if (mappedSuperclassConfig != null)
      return mappedSuperclassConfig;

    mappedSuperclassConfig = getMappedSuperclassConfig(name);

    return mappedSuperclassConfig;
  }

  static AttributeOverrideConfig convertAttributeOverrideAnnotationToConfig(Annotation attOverrideAnn)
  {
    /*
    Column columnAnn = attOverrideAnn.getAnnotation("column");

    return createAttributeOverrideConfig(attOverrideAnn.getString("name"),
                                         columnAnn.getString("name"),
                                         columnAnn.getBoolean("nullable"),
                                         columnAnn.getBoolean("unique"));
     */
    throw new UnsupportedOperationException();
  }

  static AttributeOverrideConfig createAttributeOverrideConfig(String name,
                                                                       String columnName,
                                                                       boolean isNullable,
                                                                       boolean isUnique)
  {
    AttributeOverrideConfig attOverrideConfig
      = new AttributeOverrideConfig();

    attOverrideConfig.setName(name);

    ColumnConfig columnConfig = new ColumnConfig();

    columnConfig.setName(columnName);
    columnConfig.setNullable(isNullable);
    columnConfig.setUnique(isUnique);

    attOverrideConfig.setColumn(columnConfig);

    return attOverrideConfig;
  }
}
