/*
* JBoss, Home of Professional Open Source
* Copyright 2005, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.jboss.ejb.plugins.cmp.jdbc;

import java.lang.reflect.Method;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.List;
import java.util.Set;

import javax.ejb.EJBException;

import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCCMPFieldMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCCMPFieldPropertyMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCTypeMappingMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCValueClassMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCValuePropertyMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCUserTypeMappingMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCMappingMetaData;
import org.jboss.deployment.DeploymentException;
//import org.jboss.logging.Logger;

/**
 * JDBCTypeFactory mapps Java Classes to JDBCType objects.  The main job of
 * this class is to flatten the JDBCValueClassMetaData into columns.
 *
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @version $Revision$
 */
public final class JDBCTypeFactory
{
   //private static final Logger log = Logger.getLogger(JDBCTypeFactory.class);

   //
   // Default CMPFieldStateFactory implementations
   //

   /**
    * This implementation uses field's value as its state.
    */
   public static final CMPFieldStateFactory EQUALS = new CMPFieldStateFactory()
   {
      public Object getFieldState(Object fieldValue)
      {
         return fieldValue;
      }

      public boolean isStateValid(Object state, Object fieldValue)
      {
         return state == null ? fieldValue == null : state.equals(fieldValue);
      }
   };

   /**
    * This implementation will always suppose that the state is invalid unless
    * both states are null.
    */
   private static final CMPFieldStateFactory INVALID_UNLESS_NULL = new CMPFieldStateFactory()
   {
      public Object getFieldState(Object fieldValue)
      {
         return fieldValue;
      }

      public boolean isStateValid(Object state, Object fieldValue)
      {
         return state == null ? fieldValue == null : false;
      }
   };

   /**
    * Field state factory for java.util.Map implementations. The state is
    * a deep copy of the value.
    */
   private static final CMPFieldStateFactory MAP = new CMPFieldStateFactory()
   {
      public Object getFieldState(Object fieldValue)
      {
         return cloneValue(fieldValue, Map.class);
      }

      public boolean isStateValid(Object state, Object fieldValue)
      {
         return (state == null ? fieldValue == null : state.equals(fieldValue));
      }
   };

   /**
    * Field state factory for java.util.List implementations. The state is
    * a deep copy of the value.
    */
   private static final CMPFieldStateFactory LIST = new CMPFieldStateFactory()
   {
      public Object getFieldState(Object fieldValue)
      {
         return cloneValue(fieldValue, Collection.class);
      }

      public boolean isStateValid(Object state, Object fieldValue)
      {
         return (state == null ? fieldValue == null : state.equals(fieldValue));
      }
   };

   /**
    * Field state factory for java.util.Set implementations. The state is
    * a deep copy of the value.
    */
   private static final CMPFieldStateFactory SET = new CMPFieldStateFactory()
   {
      public Object getFieldState(Object fieldValue)
      {
         return cloneValue(fieldValue, Collection.class);
      }

      public boolean isStateValid(Object state, Object fieldValue)
      {
         return (state == null ? fieldValue == null : state.equals(fieldValue));
      }
   };

   /**
    * Field state factory for arrays. The state is a deep copy of the value.
    */
   private static final CMPFieldStateFactory ARRAY = new CMPFieldStateFactory()
   {
      public Object getFieldState(Object fieldValue)
      {
         Object state = null;
         if(fieldValue != null)
         {
            int length = Array.getLength(fieldValue);
            state = Array.newInstance(fieldValue.getClass().getComponentType(), length);
            System.arraycopy(fieldValue, 0, state, 0, length);
         }
         return state;
      }

      public boolean isStateValid(Object state, Object fieldValue)
      {
         boolean valid;
         if(state == null)
         {
            valid = fieldValue == null;
         }
         else
         {
            if(fieldValue == null)
            {
               valid = false;
            }
            else
            {
               int stateLength = Array.getLength(state);
               if(stateLength != Array.getLength(fieldValue))
               {
                  valid = false;
               }
               else
               {
                  valid = true;
                  for(int i = 0; i < stateLength; ++i)
                  {
                     Object stateEl = Array.get(state, i);
                     Object valueEl = Array.get(fieldValue, i);
                     valid = (stateEl == null ? valueEl == null : stateEl.equals(valueEl));
                     if(!valid)
                     {
                        break;
                     }
                  }
               }
            }
         }
         return valid;
      }
   };

   //
   // Static
   //

   public static final CMPFieldStateFactory getCMPFieldStateFactory(JDBCTypeFactory factory,
                                                                    String implClassName,
                                                                    Class clazz)
      throws DeploymentException
   {
      CMPFieldStateFactory stateFactory;

      // if the state factory is not provided on the field level use the one from the user type mapping if any
      if(implClassName == null)
      {
         JDBCUserTypeMappingMetaData userMapping = (JDBCUserTypeMappingMetaData)factory.userTypeMappings.get(clazz.getName());
         if(userMapping != null)
         {
            implClassName = userMapping.getStateFactory();
         }
      }

      if(implClassName != null)
      {
         try
         {
            Class implClass = TCLAction.UTIL.getContextClassLoader().loadClass(implClassName);
            stateFactory = (CMPFieldStateFactory)implClass.newInstance();
         }
         catch(ClassNotFoundException e)
         {
            throw new DeploymentException("Could not load state factory class: " + implClassName);
         }
         catch(Exception e)
         {
            throw new DeploymentException("Failed instantiate state factory: " + implClassName);
         }
      }
      else if(Map.class.isAssignableFrom(clazz))
      {
         stateFactory = MAP;
      }
      else if(List.class.isAssignableFrom(clazz))
      {
         stateFactory = LIST;
      }
      else if(Set.class.isAssignableFrom(clazz))
      {
         stateFactory = SET;
      }
      else if(clazz.isArray())
      {
         stateFactory = ARRAY;
      }
      else if(usedWithEqualsStateFactory(clazz))
      {
         stateFactory = EQUALS;
      }
      else
      {
         stateFactory = INVALID_UNLESS_NULL;
      }
      return stateFactory;
   }

   public static final boolean checkDirtyAfterGet(JDBCTypeFactory factory, byte checkDirtyAfterGet, Class fieldType)
   {
      boolean result;
      if(checkDirtyAfterGet == JDBCCMPFieldMetaData.CHECK_DIRTY_AFTER_GET_NOT_PRESENT)
      {
         JDBCUserTypeMappingMetaData userMapping = (JDBCUserTypeMappingMetaData)factory.
            userTypeMappings.get(fieldType.getName());
         if(userMapping != null &&
            userMapping.checkDirtyAfterGet() != JDBCCMPFieldMetaData.CHECK_DIRTY_AFTER_GET_NOT_PRESENT)
         {
            result = userMapping.checkDirtyAfterGet() == JDBCCMPFieldMetaData.CHECK_DIRTY_AFTER_GET_TRUE;
         }
         else
         {
            result = !isDefaultImmutable(fieldType);
         }
      }
      else
      {
         result = checkDirtyAfterGet == JDBCCMPFieldMetaData.CHECK_DIRTY_AFTER_GET_TRUE;
      }
      return result;
   }

   private static Object cloneValue(Object fieldValue, Class argType)
   {
      if(fieldValue == null)
      {
         return null;
      }

      Class valueType = fieldValue.getClass();
      Constructor ctor;
      try
      {
         ctor = valueType.getConstructor(new Class[]{argType});
      }
      catch(NoSuchMethodException e)
      {
         throw new IllegalStateException(
            "Failed to find a ctor in " + valueType +
            " that takes an instance of " + argType + " as an argument."
         );
      }

      try
      {
         return ctor.newInstance(new Object[]{fieldValue});
      }
      catch(Exception e)
      {
         throw new IllegalStateException(
            "Failed to create an instance of " + valueType +
            " with the " + fieldValue + " as a ctor argument"
         );
      }
   }

   private static final boolean usedWithEqualsStateFactory(Class clazz)
   {
      return
         isDefaultImmutable(clazz) ||
         clazz == java.util.Date.class ||
         clazz == java.sql.Date.class ||
         clazz == java.sql.Time.class ||
         clazz == java.sql.Timestamp.class;
   }

   private static final boolean isDefaultImmutable(Class clazz)
   {
      boolean result = false;
      if(clazz.isPrimitive()
         || clazz == Boolean.class
         || clazz == Byte.class
         || clazz == Short.class
         || clazz == Integer.class
         || clazz == Long.class
         || clazz == Float.class
         || clazz == Double.class
         || clazz == Character.class
         || clazz == String.class
         || clazz == java.math.BigInteger.class
         || clazz == java.math.BigDecimal.class
      )
      {
         result = true;
      }
      return result;
   }

   //
   // Attributes
   //

   // the type mapping to use with the specified database
   private final JDBCTypeMappingMetaData typeMapping;

   // all known complex types by java class type
   private final Map complexTypes = new HashMap();
   private final Map mappedSimpleTypes = new HashMap();

   /** user types mappings */
   private final Map userTypeMappings;

   public JDBCTypeFactory(JDBCTypeMappingMetaData typeMapping,
                          Collection valueClasses,
                          Map userTypeMappings) throws DeploymentException
   {
      this.typeMapping = typeMapping;
      this.userTypeMappings = userTypeMappings;

      HashMap valueClassesByType = new HashMap();
      for(Iterator i = valueClasses.iterator(); i.hasNext();)
      {
         JDBCValueClassMetaData valueClass = (JDBCValueClassMetaData)i.next();
         valueClassesByType.put(valueClass.getJavaType(), valueClass);
      }


      // convert the value class meta data to a jdbc complex type
      for(Iterator i = valueClasses.iterator(); i.hasNext();)
      {
         JDBCValueClassMetaData valueClass = (JDBCValueClassMetaData)i.next();
         JDBCTypeComplex type = createTypeComplex(valueClass, valueClassesByType);
         complexTypes.put(valueClass.getJavaType(), type);
      }

      Iterator i = typeMapping.getMappings().iterator();
      while(i.hasNext())
      {
         JDBCMappingMetaData mapping = (JDBCMappingMetaData)i.next();

         String sqlType = mapping.getSqlType();
         int jdbcType = mapping.getJdbcType();
         Class javaType = loadClass(mapping.getJavaType());
         boolean notNull = javaType.isPrimitive();
         boolean autoIncrement = false;

         JDBCParameterSetter paramSetter;
         if(mapping.getParamSetter() != null)
         {
            paramSetter = (JDBCParameterSetter)newInstance(mapping.getParamSetter());
         }
         else
         {
            paramSetter = JDBCUtil.getParameterSetter(jdbcType, javaType);
         }

         JDBCResultSetReader resultReader;
         if(mapping.getResultReader() != null)
         {
            resultReader = (JDBCResultSetReader)newInstance(mapping.getResultReader());
         }
         else
         {
            resultReader = JDBCUtil.getResultSetReader(jdbcType, javaType);
         }

         JDBCTypeSimple type = new JDBCTypeSimple(
            null, javaType, jdbcType, sqlType, notNull, autoIncrement, null, paramSetter, resultReader
         );
         mappedSimpleTypes.put(javaType, type);
      }
   }

   public JDBCType getJDBCType(Class javaType)
   {
      if(complexTypes.containsKey(javaType))
      {
         return (JDBCTypeComplex)complexTypes.get(javaType);
      }
      else
      {
         JDBCTypeSimple type = (JDBCTypeSimple)mappedSimpleTypes.get(javaType);
         if(type == null)
         {
            JDBCUserTypeMappingMetaData userTypeMapping =
               (JDBCUserTypeMappingMetaData)userTypeMappings.get(javaType.getName());
            Mapper mapper = null;
            if(userTypeMapping != null)
            {
               ClassLoader cl = Thread.currentThread().getContextClassLoader();
               try
               {
                  javaType = cl.loadClass(userTypeMapping.getMappedType());
               }
               catch(ClassNotFoundException e)
               {
                  throw new IllegalStateException("Failed to load mapped type: " + userTypeMapping.getMappedType());
               }

               try
               {
                  mapper = (Mapper)newInstance(userTypeMapping.getMapper());
               }
               catch(DeploymentException e)
               {
                  throw new IllegalStateException("Failed to create Mapper instance of " + userTypeMapping.getMapper());
               }
            }

            JDBCMappingMetaData typeMappingMD = typeMapping.getTypeMappingMetaData(javaType);
            String sqlType = typeMappingMD.getSqlType();
            int jdbcType = typeMappingMD.getJdbcType();
            boolean notNull = javaType.isPrimitive();
            boolean autoIncrement = false;

            JDBCParameterSetter paramSetter;
            if(typeMappingMD.getParamSetter() != null)
            {
               try
               {
                  paramSetter = (JDBCParameterSetter)newInstance(typeMappingMD.getParamSetter());
               }
               catch(DeploymentException e)
               {
                  throw new IllegalStateException(e.getMessage());
               }
            }
            else
            {
               paramSetter = JDBCUtil.getParameterSetter(jdbcType, javaType);
            }

            JDBCResultSetReader resultReader;
            if(typeMappingMD.getResultReader() != null)
            {
               try
               {
                  resultReader = (JDBCResultSetReader)newInstance(typeMappingMD.getResultReader());
               }
               catch(DeploymentException e)
               {
                  throw new IllegalStateException(e.getMessage());
               }
            }
            else
            {
               resultReader = JDBCUtil.getResultSetReader(jdbcType, javaType);
            }

            type = new JDBCTypeSimple(
               null, javaType, jdbcType, sqlType, notNull, autoIncrement, mapper, paramSetter, resultReader
            );
         }
         return type;
      }
   }

   public JDBCType getJDBCType(JDBCCMPFieldMetaData cmpField) throws DeploymentException
   {
      JDBCType fieldJDBCType;
      final Class fieldType = cmpField.getFieldType();
      if(complexTypes.containsKey(fieldType))
      {
         fieldJDBCType = createTypeComplex(cmpField);
      }
      else
      {
         fieldJDBCType = createTypeSimple(cmpField);
      }
      return fieldJDBCType;
   }

   public int getJDBCTypeForJavaType(Class clazz)
   {
      return typeMapping.getTypeMappingMetaData(clazz).getJdbcType();
   }

   public JDBCTypeMappingMetaData getTypeMapping()
   {
      return typeMapping;
   }

   private JDBCTypeComplex createTypeComplex(
      JDBCValueClassMetaData valueClass,
      HashMap valueClassesByType)
   {
      // get the properties
      ArrayList propertyList = createComplexProperties(valueClass, valueClassesByType, new PropertyStack());

      // transform properties into an array
      JDBCTypeComplexProperty[] properties = new JDBCTypeComplexProperty[propertyList.size()];
      properties = (JDBCTypeComplexProperty[])propertyList.toArray(properties);

      return new JDBCTypeComplex(properties, valueClass.getJavaType());
   }

   private JDBCTypeSimple createTypeSimple(JDBCCMPFieldMetaData cmpField) throws DeploymentException
   {
      String columnName = cmpField.getColumnName();
      Class javaType = cmpField.getFieldType();

      JDBCMappingMetaData typeMappingMD = typeMapping.getTypeMappingMetaData(javaType);
      String paramSetter = typeMappingMD.getParamSetter();
      String resultReader = typeMappingMD.getResultReader();

      int jdbcType;
      String sqlType = cmpField.getSQLType();
      if(sqlType != null)
      {
         jdbcType = cmpField.getJDBCType();
      }
      else
      {
         // get jdbcType and sqlType from typeMapping
         sqlType = typeMappingMD.getSqlType();
         jdbcType = typeMappingMD.getJdbcType();
      }

      boolean notNull = cmpField.isNotNull();
      boolean autoIncrement = cmpField.isAutoIncrement();

      Mapper mapper = null;
      JDBCUserTypeMappingMetaData userTypeMapping = (JDBCUserTypeMappingMetaData)userTypeMappings.get(javaType.getName());
      if(userTypeMapping != null)
      {
         String mappedTypeStr = userTypeMapping.getMappedType();
         try
         {
            final ClassLoader contextClassLoader = TCLAction.UTIL.getContextClassLoader();
            Class mapperClass = contextClassLoader.loadClass(userTypeMapping.getMapper());
            mapper = (Mapper)mapperClass.newInstance();
            javaType = contextClassLoader.loadClass(mappedTypeStr);
            if(cmpField.getSQLType() == null)
            {
               JDBCMappingMetaData mappingMD = typeMapping.getTypeMappingMetaData(javaType);
               sqlType = mappingMD.getSqlType();
               jdbcType = mappingMD.getJdbcType();
               paramSetter = mappingMD.getParamSetter();
               resultReader = mappingMD.getResultReader();
            }
         }
         catch(ClassNotFoundException e)
         {
            throw new DeploymentException("Class not found for mapper: " + userTypeMapping.getMapper(), e);
         }
         catch(Exception e)
         {
            throw new DeploymentException("Could not instantiate mapper: " + userTypeMapping.getMapper(), e);
         }
      }

      JDBCParameterSetter paramSetterImpl;
      if(paramSetter == null)
      {
         paramSetterImpl = JDBCUtil.getParameterSetter(jdbcType, javaType);
      }
      else
      {
         paramSetterImpl = (JDBCParameterSetter)newInstance(paramSetter);
      }

      JDBCResultSetReader resultReaderImpl;
      if(resultReader == null)
      {
         resultReaderImpl = JDBCUtil.getResultSetReader(jdbcType, javaType);
      }
      else
      {
         resultReaderImpl = (JDBCResultSetReader)newInstance(resultReader);
      }

      return new JDBCTypeSimple(
         columnName,
         javaType,
         jdbcType,
         sqlType,
         notNull,
         autoIncrement,
         mapper,
         paramSetterImpl,
         resultReaderImpl
      );
   }

   private JDBCTypeComplex createTypeComplex(JDBCCMPFieldMetaData cmpField)
   {
      // get the default properties for a field of its type
      JDBCTypeComplex type = (JDBCTypeComplex)complexTypes.get(cmpField.getFieldType());
      JDBCTypeComplexProperty[] defaultProperties = type.getProperties();

      // create a map of the overrides based on flat property name
      HashMap overrides = new HashMap();

      for(int i = 0; i < cmpField.getPropertyOverrides().size(); ++i)
      {
         JDBCCMPFieldPropertyMetaData p = (JDBCCMPFieldPropertyMetaData)cmpField.getPropertyOverrides().get(i);
         overrides.put(p.getPropertyName(), p);
      }

      // array that will hold the final properites after overrides
      JDBCTypeComplexProperty[] finalProperties = new JDBCTypeComplexProperty[defaultProperties.length];

      // override property default values
      for(int i = 0; i < defaultProperties.length; i++)
      {
         // pop off the override, if present
         JDBCCMPFieldPropertyMetaData override;
         override = (JDBCCMPFieldPropertyMetaData)overrides.remove(defaultProperties[i].getPropertyName());

         if(override == null)
         {
            finalProperties[i] = defaultProperties[i];
            finalProperties[i] = new JDBCTypeComplexProperty(
               defaultProperties[i],
               cmpField.getColumnName() + "_" +
               defaultProperties[i].getColumnName(),
               defaultProperties[i].getJDBCType(),
               defaultProperties[i].getSQLType(),
               cmpField.isNotNull() || defaultProperties[i].isNotNull());
         }
         else
         {
            // columnName
            String columnName = override.getColumnName();
            if(columnName == null)
            {
               columnName = cmpField.getColumnName() + "_" + defaultProperties[i].getColumnName();
            }

            // sql and jdbc type
            String sqlType = override.getSQLType();
            int jdbcType;
            if(sqlType != null)
            {
               jdbcType = override.getJDBCType();
            }
            else
            {
               sqlType = defaultProperties[i].getSQLType();
               jdbcType = defaultProperties[i].getJDBCType();
            }

            boolean notNull = cmpField.isNotNull() ||
               override.isNotNull() ||
               defaultProperties[i].isNotNull();

            finalProperties[i] = new JDBCTypeComplexProperty(
               defaultProperties[i],
               columnName,
               jdbcType,
               sqlType,
               notNull);
         }
      }

      // did we find all overriden properties
      if(overrides.size() > 0)
      {
         String propertyName = (String)overrides.keySet().iterator().next();
         throw new EJBException("Property " + propertyName + " in field " +
            cmpField.getFieldName() + " is not a property of value object " +
            cmpField.getFieldType().getName());
      }

      // return the new complex type
      return new JDBCTypeComplex(finalProperties, cmpField.getFieldType());
   }

   private ArrayList createComplexProperties(
      JDBCValueClassMetaData valueClass,
      HashMap valueClassesByType,
      PropertyStack propertyStack)
   {

      ArrayList properties = new ArrayList();

      // add the properties each property to the list
      java.util.List valueClassProperties = valueClass.getProperties();
      for(int i = 0; i < valueClassProperties.size(); ++i)
      {
         JDBCValuePropertyMetaData propertyMetaData =
            (JDBCValuePropertyMetaData)valueClassProperties.get(i);
         properties.addAll(createComplexProperties(propertyMetaData,
            valueClassesByType, propertyStack));
      }
      return properties;
   }

   private ArrayList createComplexProperties(
      JDBCValuePropertyMetaData propertyMetaData,
      HashMap valueClassesByType,
      PropertyStack propertyStack)
   {

      // push my data onto the stack
      propertyStack.pushPropertyMetaData(propertyMetaData);

      ArrayList properties = new ArrayList();

      Class javaType = propertyMetaData.getPropertyType();
      if(!valueClassesByType.containsKey(javaType))
      {

         // this property is a simple type
         // which makes this the end of the line for recursion
         String propertyName = propertyStack.getPropertyName();
         String columnName = propertyStack.getColumnName();

         String sqlType = propertyMetaData.getSqlType();
         int jdbcType;
         if(sqlType != null)
         {
            jdbcType = propertyMetaData.getJDBCType();
         }
         else
         {
            // get jdbcType and sqlType from typeMapping
            JDBCMappingMetaData typeMappingMD = typeMapping.getTypeMappingMetaData(javaType);
            sqlType = typeMappingMD.getSqlType();
            jdbcType = typeMappingMD.getJdbcType();
         }

         boolean notNull = propertyStack.isNotNull();

         Method[] getters = propertyStack.getGetters();
         Method[] setters = propertyStack.getSetters();

         properties.add(new JDBCTypeComplexProperty(
            propertyName,
            columnName,
            javaType,
            jdbcType,
            sqlType,
            notNull,
            getters,
            setters));

      }
      else
      {

         // this property is a value object, recurse
         JDBCValueClassMetaData valueClass =
            (JDBCValueClassMetaData)valueClassesByType.get(javaType);
         properties.addAll(createComplexProperties(
            valueClass,
            valueClassesByType,
            propertyStack));

      }

      // pop my data, back off
      propertyStack.popPropertyMetaData();

      return properties;
   }

   private static final class PropertyStack
   {
      final ArrayList properties = new ArrayList();
      final ArrayList propertyNames = new ArrayList();
      final ArrayList columnNames = new ArrayList();
      final ArrayList notNulls = new ArrayList();
      final ArrayList getters = new ArrayList();
      final ArrayList setters = new ArrayList();

      public PropertyStack()
      {
      }

      public final void pushPropertyMetaData(
         JDBCValuePropertyMetaData propertyMetaData)
      {

         propertyNames.add(propertyMetaData.getPropertyName());
         columnNames.add(propertyMetaData.getColumnName());
         notNulls.add(new Boolean(propertyMetaData.isNotNull()));
         getters.add(propertyMetaData.getGetter());
         setters.add(propertyMetaData.getSetter());

         if(properties.contains(propertyMetaData))
         {
            throw new EJBException("Circular reference discoverd at " +
               "property: " + getPropertyName());
         }
         properties.add(propertyMetaData);
      }

      public final void popPropertyMetaData()
      {
         propertyNames.remove(propertyNames.size() - 1);
         columnNames.remove(columnNames.size() - 1);
         notNulls.remove(notNulls.size() - 1);
         getters.remove(getters.size() - 1);
         setters.remove(setters.size() - 1);

         properties.remove(properties.size() - 1);
      }

      public final String getPropertyName()
      {
         StringBuffer buf = new StringBuffer();
         for(int i = 0; i < propertyNames.size(); i++)
         {
            if(i > 0)
            {
               buf.append(".");
            }
            buf.append((String)propertyNames.get(i));
         }
         return buf.toString();
      }

      public final String getColumnName()
      {
         StringBuffer buf = new StringBuffer();
         for(int i = 0; i < columnNames.size(); i++)
         {
            if(i > 0)
            {
               buf.append("_");
            }
            buf.append((String)columnNames.get(i));
         }
         return buf.toString();
      }

      public final boolean isNotNull()
      {
         for(int i = 0; i < notNulls.size(); i++)
         {
            if(((Boolean)notNulls.get(i)).booleanValue())
            {
               return true;
            }
         }
         return false;
      }

      public final Method[] getGetters()
      {
         return (Method[])getters.toArray(new Method[getters.size()]);
      }

      public final Method[] getSetters()
      {
         return (Method[])setters.toArray(new Method[setters.size()]);
      }
   }


   private Object newInstance(String className) throws DeploymentException
   {
      Class clazz = loadClass(className);
      try
      {
         return clazz.newInstance();
      }
      catch(Exception e)
      {
         throw new DeploymentException("Failed to instantiate " + className, e);
      }
   }
   private Class loadClass(String className) throws DeploymentException
   {
      try
      {
         final ClassLoader contextClassLoader = TCLAction.UTIL.getContextClassLoader();
         return contextClassLoader.loadClass(className);
      }
      catch(ClassNotFoundException e)
      {
         throw new DeploymentException("Failed to load class: " + className, e);
      }
   }
}
