/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file 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.metadata;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;

import org.w3c.dom.Element;

import org.jboss.deployment.DeploymentException;
import org.jboss.metadata.ApplicationMetaData;
import org.jboss.metadata.BeanMetaData;
import org.jboss.metadata.EntityMetaData;
import org.jboss.metadata.MetaData;
import org.jboss.metadata.RelationMetaData;

import org.jboss.ejb.plugins.cmp.jdbc.SQLUtil;

/**
 * This immutable class contains information about the application
 *
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="sebastien.alborini@m4x.org">Sebastien Alborini</a>
 * @author <a href="alex@jboss.org">Alexey Loubyansky</a>
 * @version $Revision: 81030 $
 */
public final class JDBCApplicationMetaData
{
   private final static Class JDBC_PM = org.jboss.ejb.plugins.cmp.jdbc.JDBCStoreManager.class;

   /**
    * The class loader for this application.  The class loader is used to
    * load all classes used by this application.
    */
   private final ClassLoader classLoader;

   /**
    * Application metadata loaded from the ejb-jar.xml file
    */
   private final ApplicationMetaData applicationMetaData;

   /**
    * Map with user defined type mapping, e.g. enum mappings
    */
   private final Map userTypeMappings;

   /**
    * Map of the type mappings by name.
    */
   private final Map typeMappings = new HashMap();

   /**
    * Map of the entities managed by jbosscmp-jdbc by bean name.
    */
   private final Map entities = new HashMap();

   /**
    * Collection of relations in this application.
    */
   private final Collection relationships = new HashSet();

   /**
    * Map of the collection relationship roles for each entity by entity object.
    */
   private final Map entityRoles = new HashMap();

   /**
    * Map of the dependent value classes by java class type.
    */
   private final Map valueClasses = new HashMap();

   /**
    * Map from abstract schema name to entity name
    */
   private final Map entitiesByAbstractSchemaName = new HashMap();

   /**
    * Map from entity interface(s) java type to entity name
    */
   private final Map entitiesByInterface = new HashMap();

   /**
    * Map of the entity commands by name.
    */
   private final Map entityCommands = new HashMap();

   /**
    * Constructs jdbc application meta data with the data from the
    * applicationMetaData.
    *
    * @param applicationMetaData the application data loaded from
    *    the ejb-jar.xml file
    * @param classLoader the ClassLoader used to load the classes
    *    of the application
    * @throws DeploymentException if an problem occures while loading
    *    the classes or if data in the ejb-jar.xml is inconsistent
    *    with data from jbosscmp-jdbc.xml file
    */
   public JDBCApplicationMetaData(ApplicationMetaData applicationMetaData, ClassLoader classLoader)
      throws DeploymentException
   {
      // the classloader is the same for all the beans in the application
      this.classLoader = classLoader;
      this.applicationMetaData = applicationMetaData;

      // create metadata for all jbosscmp-jdbc-managed cmp entities
      // we do that here in case there is no jbosscmp-jdbc.xml
      Iterator beans = applicationMetaData.getEnterpriseBeans();
      while(beans.hasNext())
      {
         BeanMetaData bean = (BeanMetaData)beans.next();

         // only take entities
         if(bean.isEntity())
         {
            EntityMetaData entity = (EntityMetaData)bean;

            // only take jbosscmp-jdbc-managed CMP entities
            Class pm;
            try
            {
               pm = classLoader.loadClass(entity.getContainerConfiguration().getPersistenceManager());
            }
            catch (ClassNotFoundException e)
            {
               throw new DeploymentException("Unable to load persistence manager", e);
            }
            if(entity.isCMP() &&
               (JDBC_PM.isAssignableFrom(pm) || pm.getName().equals("org.jboss.ejb.plugins.cmp.jdbc2.JDBCStoreManager2")))
            {
               JDBCEntityMetaData jdbcEntity = new JDBCEntityMetaData(this, entity);

               entities.put(entity.getEjbName(), jdbcEntity);

               String schemaName = jdbcEntity.getAbstractSchemaName();
               if(schemaName != null)
               {
                  entitiesByAbstractSchemaName.put(schemaName, jdbcEntity);
               }

               Class remote = jdbcEntity.getRemoteClass();
               if(remote != null)
               {
                  entitiesByInterface.put(remote, jdbcEntity);
               }

               Class local = jdbcEntity.getLocalClass();
               if(local != null)
               {
                  entitiesByInterface.put(local, jdbcEntity);
               }

               // initialized the entity roles collection
               entityRoles.put(entity.getEjbName(), new HashSet());
            }
         }
      }

      // relationships
      Iterator iterator = applicationMetaData.getRelationships();
      while(iterator.hasNext())
      {
         RelationMetaData relation = (RelationMetaData)iterator.next();

         // Relationship metadata
         JDBCRelationMetaData jdbcRelation =
            new JDBCRelationMetaData(this, relation);
         relationships.add(jdbcRelation);

         // Left relationship-role metadata
         JDBCRelationshipRoleMetaData left =
            jdbcRelation.getLeftRelationshipRole();
         Collection leftEntityRoles =
            (Collection)entityRoles.get(left.getEntity().getName());
         leftEntityRoles.add(left);

         // Right relationship-role metadata
         JDBCRelationshipRoleMetaData right =
            jdbcRelation.getRightRelationshipRole();
         Collection rightEntityRoles =
            (Collection)entityRoles.get(right.getEntity().getName());
         rightEntityRoles.add(right);
      }

      userTypeMappings = Collections.EMPTY_MAP;
   }

   /**
    * Constructs application meta data with the data contained in the
    * jboss-cmp xml element from a jbosscmp-jdbc xml file. Optional values
    * of the xml element that are not present are loaded from the
    * defalutValues parameter.
    *
    * @param element the xml Element which contains the metadata about
    *    this application
    * @param defaultValues the JDBCApplicationMetaData which contains
    *    the values
    *      for optional elements of the element
    * @throws DeploymentException if the xml element is not semantically correct
    */
   public JDBCApplicationMetaData(Element element, JDBCApplicationMetaData defaultValues)
      throws DeploymentException
   {
      // importXml will be called at least once: with standardjbosscmp-jdbc.xml
      // it may be called a second time with user-provided jbosscmp-jdbc.xml
      // we must ensure to set all defaults values in the first call

      classLoader = defaultValues.classLoader;
      applicationMetaData = defaultValues.applicationMetaData;

      Element userTypeMaps = MetaData.getOptionalChild(element, "user-type-mappings");
      if(userTypeMaps != null)
      {
         userTypeMappings = new HashMap();
         Iterator iter = MetaData.getChildrenByTagName(userTypeMaps, "user-type-mapping");
         while(iter.hasNext())
         {
            Element userTypeMappingEl = (Element)iter.next();
            JDBCUserTypeMappingMetaData userTypeMapping = new JDBCUserTypeMappingMetaData(userTypeMappingEl);
            userTypeMappings.put(userTypeMapping.getJavaType(), userTypeMapping);
         }
      }
      else
         userTypeMappings = defaultValues.getUserTypeMappings();

      // type-mappings: (optional, always set in standardjbosscmp-jdbc.xml)
      typeMappings.putAll(defaultValues.typeMappings);
      Element typeMaps = MetaData.getOptionalChild(element, "type-mappings");
      if(typeMaps != null)
      {
         for(Iterator i = MetaData.getChildrenByTagName(typeMaps, "type-mapping"); i.hasNext();)
         {
            Element typeMappingElement = (Element)i.next();
            JDBCTypeMappingMetaData typeMapping =
               new JDBCTypeMappingMetaData(typeMappingElement);
            typeMappings.put(typeMapping.getName(), typeMapping);
         }
      }

      // dependent-value-objects
      valueClasses.putAll(defaultValues.valueClasses);
      Element valueClassesElement =
         MetaData.getOptionalChild(element, "dependent-value-classes");
      if(valueClassesElement != null)
      {
         for(Iterator i = MetaData.getChildrenByTagName(
            valueClassesElement, "dependent-value-class");
             i.hasNext();)
         {

            Element valueClassElement = (Element)i.next();
            JDBCValueClassMetaData valueClass =
               new JDBCValueClassMetaData(valueClassElement, classLoader);
            valueClasses.put(valueClass.getJavaType(), valueClass);
         }
      }

      // entity-commands: (optional, always set in standardjbosscmp-jdbc.xml)
      entityCommands.putAll(defaultValues.entityCommands);
      Element entityCommandMaps = MetaData.getOptionalChild(
         element, "entity-commands");
      if(entityCommandMaps != null)
      {
         for(Iterator i =
            MetaData.getChildrenByTagName(entityCommandMaps, "entity-command");
             i.hasNext();)
         {

            Element entityCommandElement = (Element)i.next();
            JDBCEntityCommandMetaData entityCommand =
               new JDBCEntityCommandMetaData(entityCommandElement);
            entityCommands.put(entityCommand.getCommandName(), entityCommand);
         }
      }
      
      // reserved words: (optional, always set in standardjbosscmp-jdbc.xml)
      // list of reserved words that should be escaped in table names 
      Element rWords = MetaData.getOptionalChild(element,"reserved-words");
      if (rWords!=null) 
      {
      	for (Iterator i = MetaData.getChildrenByTagName(rWords,"word"); i.hasNext() ; ) 
      	{
      			Element rWord = (Element)i.next();
      			SQLUtil.addToRwords(MetaData.getElementContent(rWord));      				
      	}
      }

      // defaults: apply defaults for entities (optional, always
      // set in standardjbosscmp-jdbc.xml)
      entities.putAll(defaultValues.entities);
      entitiesByAbstractSchemaName.putAll(
         defaultValues.entitiesByAbstractSchemaName);
      entitiesByInterface.putAll(defaultValues.entitiesByInterface);
      Element defaults = MetaData.getOptionalChild(element, "defaults");
      if(defaults != null)
      {
         ArrayList values = new ArrayList(entities.values());
         for(Iterator i = values.iterator(); i.hasNext();)
         {
            JDBCEntityMetaData entityMetaData = (JDBCEntityMetaData)i.next();

            // create the new metadata with the defaults applied
            entityMetaData =
               new JDBCEntityMetaData(this, defaults, entityMetaData);

            // replace the old meta data with the new
            entities.put(entityMetaData.getName(), entityMetaData);

            String schemaName = entityMetaData.getAbstractSchemaName();
            if(schemaName != null)
            {
               entitiesByAbstractSchemaName.put(schemaName, entityMetaData);
            }

            Class remote = entityMetaData.getRemoteClass();
            if(remote != null)
            {
               entitiesByInterface.put(remote, entityMetaData);
            }

            Class local = entityMetaData.getLocalClass();
            if(local != null)
            {
               entitiesByInterface.put(local, entityMetaData);
            }
         }
      }

      // enterprise-beans: apply entity specific configuration
      // (only in jbosscmp-jdbc.xml)
      Element enterpriseBeans =
         MetaData.getOptionalChild(element, "enterprise-beans");
      if(enterpriseBeans != null)
      {
         for(Iterator i =
            MetaData.getChildrenByTagName(enterpriseBeans, "entity");
             i.hasNext();)
         {

            Element beanElement = (Element)i.next();

            // get entity by name, if not found, it is a config error
            String ejbName =
               MetaData.getUniqueChildContent(beanElement, "ejb-name");
            JDBCEntityMetaData entityMetaData = getBeanByEjbName(ejbName);

            if(entityMetaData == null)
            {
               throw new DeploymentException("Configuration found in " +
                  "jbosscmp-jdbc.xml for entity " + ejbName + " but bean " +
                  "is not a jbosscmp-jdbc-managed cmp entity in " +
                  "ejb-jar.xml");
            }
            entityMetaData =
               new JDBCEntityMetaData(this, beanElement, entityMetaData);
            entities.put(entityMetaData.getName(), entityMetaData);

            String schemaName = entityMetaData.getAbstractSchemaName();
            if(schemaName != null)
            {
               entitiesByAbstractSchemaName.put(schemaName, entityMetaData);
            }

            Class remote = entityMetaData.getRemoteClass();
            if(remote != null)
            {
               entitiesByInterface.put(remote, entityMetaData);
            }

            Class local = entityMetaData.getLocalClass();
            if(local != null)
            {
               entitiesByInterface.put(local, entityMetaData);
            }
         }
      }

      // defaults: apply defaults for relationships (optional, always
      // set in standardjbosscmp-jdbc.xml)
      if(defaults == null)
      {
         // no defaults just copy over the existing relationships and roles
         relationships.addAll(defaultValues.relationships);
         entityRoles.putAll(defaultValues.entityRoles);
      }
      else
      {

         // create a new empty role collection for each entity
         for(Iterator i = entities.values().iterator(); i.hasNext();)
         {
            JDBCEntityMetaData entity = (JDBCEntityMetaData)i.next();
            entityRoles.put(entity.getName(), new HashSet());
         }

         // for each relationship, apply defaults and store
         for(Iterator i = defaultValues.relationships.iterator();
             i.hasNext();)
         {

            JDBCRelationMetaData relationMetaData =
               (JDBCRelationMetaData)i.next();

            // create the new metadata with the defaults applied
            relationMetaData =
               new JDBCRelationMetaData(this, defaults, relationMetaData);

            // replace the old metadata with the new
            relationships.add(relationMetaData);

            // store new left role
            JDBCRelationshipRoleMetaData left =
               relationMetaData.getLeftRelationshipRole();
            Collection leftEntityRoles =
               (Collection)entityRoles.get(left.getEntity().getName());
            leftEntityRoles.add(left);

            // store new right role
            JDBCRelationshipRoleMetaData right =
               relationMetaData.getRightRelationshipRole();
            Collection rightEntityRoles =
               (Collection)entityRoles.get(right.getEntity().getName());
            rightEntityRoles.add(right);
         }
      }

      // relationships: apply entity specific configuration
      // (only in jbosscmp-jdbc.xml)
      Element relationshipsElement =
         MetaData.getOptionalChild(element, "relationships");
      if(relationshipsElement != null)
      {

         // create a map of the relations by name (if it has a name)
         Map relationByName = new HashMap();
         for(Iterator i = relationships.iterator(); i.hasNext();)
         {
            JDBCRelationMetaData relation = (JDBCRelationMetaData)i.next();
            if(relation.getRelationName() != null)
            {
               relationByName.put(relation.getRelationName(), relation);
            }
         }


         for(Iterator i = MetaData.getChildrenByTagName(relationshipsElement, "ejb-relation"); i.hasNext();)
         {
            Element relationElement = (Element)i.next();

            // get relation by name, if not found, it is a config error
            String relationName = MetaData.getUniqueChildContent(relationElement, "ejb-relation-name");
            JDBCRelationMetaData oldRelation = (JDBCRelationMetaData)relationByName.get(relationName);

            if(oldRelation == null)
            {
               throw new DeploymentException("Configuration found in " +
                  "jbosscmp-jdbc.xml for relation " + relationName +
                  " but relation is not a jbosscmp-jdbc-managed relation " +
                  "in ejb-jar.xml");
            }
            // create new metadata with relation specific config applied
            JDBCRelationMetaData newRelation =
               new JDBCRelationMetaData(this, relationElement, oldRelation);

            // replace the old metadata with the new
            relationships.remove(oldRelation);
            relationships.add(newRelation);

            // replace the old left role with the new
            JDBCRelationshipRoleMetaData newLeft =
               newRelation.getLeftRelationshipRole();
            Collection leftEntityRoles =
               (Collection)entityRoles.get(newLeft.getEntity().getName());
            leftEntityRoles.remove(oldRelation.getLeftRelationshipRole());
            leftEntityRoles.add(newLeft);

            // replace the old right role with the new
            JDBCRelationshipRoleMetaData newRight =
               newRelation.getRightRelationshipRole();
            Collection rightEntityRoles =
               (Collection)entityRoles.get(newRight.getEntity().getName());
            rightEntityRoles.remove(oldRelation.getRightRelationshipRole());
            rightEntityRoles.add(newRight);
         }
      }

   }

   /**
    * Gets the type mapping with the specified name
    * @param name the name for the type mapping
    * @return the matching type mapping or null if not found
    */
   public JDBCTypeMappingMetaData getTypeMappingByName(String name)
   {
      return (JDBCTypeMappingMetaData)typeMappings.get(name);
   }

   /**
    * Gets the relationship roles for the entity with the specified name.
    * @param entityName the name of the entity whos roles are returned
    * @return an unmodifiable collection of JDBCRelationshipRoles
    *    of the specified entity
    */
   public Collection getRolesForEntity(String entityName)
   {
      Collection roles = (Collection)entityRoles.get(entityName);
      return Collections.unmodifiableCollection(roles);
   }

   /**
    * Gets dependent value classes that are directly managed by the container.
    * @returns an unmodifiable collection of JDBCValueClassMetaData
    */
   public Collection getValueClasses()
   {
      return Collections.unmodifiableCollection(valueClasses.values());
   }

   /**
    * Gets the classloader for this application which is used to load
    * all classes.
    * @return the ClassLoader for the application
    */
   public ClassLoader getClassLoader()
   {
      return classLoader;
   }

   /**
    * Gets the metadata for an entity bean by name.
    * @param name the name of the entity meta data to return
    * @return the entity meta data for the specified name
    */
   public JDBCEntityMetaData getBeanByEjbName(String name)
   {
      return (JDBCEntityMetaData)entities.get(name);
   }

   /**
    * Gets the entity command with the specified name
    * @param name the name for the entity-command
    * @return the matching entity command or null if not found
    */
   public JDBCEntityCommandMetaData getEntityCommandByName(String name)
   {
      return (JDBCEntityCommandMetaData)entityCommands.get(name);
   }

   public Map getUserTypeMappings()
   {
      return Collections.unmodifiableMap(userTypeMappings);
   }
}
