/*
 * 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.Constructor;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.ejb.FinderException;

import org.jboss.deployment.DeploymentException;
import org.jboss.ejb.plugins.cmp.jdbc.bridge.JDBCEntityBridge;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCAutomaticQueryMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCQueryMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCDeclaredQueryMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCDynamicQLQueryMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCJBossQLQueryMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCQlQueryMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCReadAheadMetaData;
import org.jboss.ejb.plugins.cmp.jdbc.metadata.JDBCEntityMetaData;
import org.jboss.ejb.plugins.cmp.ejbql.Catalog;
import org.jboss.logging.Logger;
import org.jboss.metadata.MetaData;
import org.w3c.dom.Element;

/**
 * Maintains a map from a query method to query command.
 *
 * @author <a href="mailto:dain@daingroup.com">Dain Sundstrom</a>
 * @author <a href="mailto:rickard.oberg@telkel.com">Rickard Öberg</a>
 * @author <a href="mailto:marc.fleury@telkel.com">Marc Fleury</a>
 * @author <a href="mailto:shevlandj@kpi.com.au">Joe Shevland</a>
 * @author <a href="mailto:justin@j-m-f.demon.co.uk">Justin Forder</a>
 * @author <a href="mailto:alex@jboss.org">Alex Loubyansky</a>
 * @version $Revision: 1.7.4.13 $
 */
public final class JDBCQueryManager
{
   private static final String FIND_BY_PK = "findByPrimaryKey";
   private static final String EJB_FIND = "ejbFind";
   private static final String FIND_ALL = "findAll";
   private static final String FIND_BY = "findBy";

   private final Map knownQueries = new HashMap();
   private final JDBCStoreManager manager;

   public JDBCQueryManager(JDBCStoreManager manager)
   {
      this.manager = manager;
   }

   public static final Class getQLCompiler(Element query, JDBCEntityMetaData entity)
      throws DeploymentException
   {
      String compiler = MetaData.getOptionalChildContent(query, "ql-compiler");
      Class impl;

      if(compiler == null || compiler.trim().length() == 0)
      {
         impl = entity.getQLCompiler();
      }
      else
      {
         try
         {
            impl = TCLAction.UTIL.getContextClassLoader().loadClass(compiler);
         }
         catch(ClassNotFoundException e)
         {
            throw new DeploymentException("Failed to load compiler implementation: " + compiler);
         }
      }
      return impl;
   }

   public static final QLCompiler getInstance(Class impl, Catalog catalog)
      throws DeploymentException
   {
      if(impl == null)
      {
         throw new DeploymentException("ql-compiler is not specified.");
      }

      final Constructor constructor;
      try
      {
         constructor = impl.getConstructor(new Class[]{Catalog.class});
      }
      catch(NoSuchMethodException e)
      {
         throw new DeploymentException("Compiler class does not have a constructor which takes " + Catalog.class.getName());
      }

      try
      {
         return (QLCompiler)constructor.newInstance(new Object[]{catalog});
      }
      catch(Exception e)
      {
         throw new DeploymentException("Failed to create an instance of " + impl.getName() + ": " + e.getMessage(), e);
      }
   }

   public JDBCQueryCommand getQueryCommand(Method queryMethod)
   throws FinderException
   {
      JDBCQueryCommand queryCommand = (JDBCQueryCommand)knownQueries.get(queryMethod);
      
      if(queryCommand == null)
      {
         throw new FinderException("Unknown query: " + queryMethod);
      }
      return queryCommand;
   }
   
   public void start() throws DeploymentException
   {
      Logger log = Logger.getLogger(
            this.getClass().getName() +
            "." +
            manager.getMetaData().getName());

      JDBCCommandFactory factory = manager.getCommandFactory();

      Class homeClass = manager.getContainer().getHomeClass();
      Class localHomeClass = manager.getContainer().getLocalHomeClass();

      //
      // findByPrimaryKey
      //
      JDBCEntityBridge entity = (JDBCEntityBridge) manager.getEntityBridge();
      if(homeClass != null)
      {
         try
         {
            // try to get the finder method on the home interface
            Method method = homeClass.getMethod(FIND_BY_PK, new Class[]{entity.getPrimaryKeyClass()});

            JDBCQueryMetaData findByPKMD = manager.getMetaData().getQueryMetaDataForMethod(method);
            JDBCReadAheadMetaData readAhead = (findByPKMD == null ?
               entity.getMetaData().getReadAhead() : findByPKMD.getReadAhead());

            // got it add it to known finders
            JDBCQueryMetaData q = new JDBCAutomaticQueryMetaData(
                  method,
                  readAhead,
               entity.getMetaData().getQLCompiler(),
               false
            );
            knownQueries.put(method, factory.createFindByPrimaryKeyQuery(q));

            if(log.isDebugEnabled())
               log.debug("Added findByPrimaryKey query command for home interface");
         } catch(NoSuchMethodException e)
         {
            throw new DeploymentException("Home interface does not have a findByPrimaryKey method");
         }
      }

      if(localHomeClass != null)
      {

         Method method;
         try
         {
            // try to get the finder method on the local home interface
            method = localHomeClass.getMethod(FIND_BY_PK, new Class[] { entity.getPrimaryKeyClass() });
         } catch(NoSuchMethodException e)
         {
            throw new DeploymentException("Local home interface does " +
                  "not have the method findByPrimaryKey(" +
                  entity.getPrimaryKeyClass().getName() + ")");
         }

         // got it add it to known finders
         JDBCQueryMetaData findByPKMD = manager.getMetaData().getQueryMetaDataForMethod(method);
         JDBCReadAheadMetaData readAhead = (findByPKMD == null ?
            entity.getMetaData().getReadAhead() : findByPKMD.getReadAhead());
         JDBCQueryMetaData q = new JDBCAutomaticQueryMetaData(method, readAhead, entity.getMetaData().getQLCompiler(), false);
         knownQueries.put(method, factory.createFindByPrimaryKeyQuery(q));

         if(log.isDebugEnabled())
            log.debug("Added findByPrimaryKey query command for local home interface");
      }

      //
      // Custom finders - Overrides defined and automatic finders.
      //
      Class ejbClass = manager.getMetaData().getEntityClass();

      Method[] customMethods = ejbClass.getMethods();
      for (int i = 0; i < customMethods.length; i++)
      {
         Method m = customMethods[i];
         String  methodName = m.getName();
         if(methodName.startsWith(EJB_FIND))
         {
            String interfaceName = 'f' +  methodName.substring(4);

            if(homeClass != null)
            {
               try
               {
                  // try to get the finder method on the home interface
                  Method interfaceMethod = homeClass.getMethod(
                        interfaceName,
                        m.getParameterTypes());

                  // got it add it to known finders
                  knownQueries.put(interfaceMethod, new JDBCCustomFinderQuery(manager, m));

                  if(log.isDebugEnabled())
                     log.debug("Added custom finder " + methodName + " on home interface");
               } catch(NoSuchMethodException e)
               {
                  // this is ok method may not be defined on this interface
               }
            }

            if(localHomeClass != null)
            {
               try
               {
                  // try to get the finder method on the local home interface
                  Method interfaceMethod = localHomeClass.getMethod(
                        interfaceName,
                        m.getParameterTypes());

                  // got it add it to known finders
                  knownQueries.put(interfaceMethod, new JDBCCustomFinderQuery(manager, m));

                  if(log.isDebugEnabled())
                     log.debug("Added custom finder " + methodName + " on local home interface");
               } catch(NoSuchMethodException e)
               {
                  // this is ok method may not be defined on this interface
               }
            }
         }
      }

      //
      // Defined finders - Overrides automatic finders.
      //
      Iterator definedFinders = manager.getMetaData().getQueries().iterator();
      while(definedFinders.hasNext())
      {
         JDBCQueryMetaData q = (JDBCQueryMetaData)definedFinders.next();

         if(!knownQueries.containsKey(q.getMethod()) )
         {
            if(q instanceof JDBCJBossQLQueryMetaData)
            {
               knownQueries.put(q.getMethod(), factory.createJBossQLQuery(q));

            } else if(q instanceof JDBCDynamicQLQueryMetaData)
            {
               knownQueries.put(q.getMethod(), factory.createDynamicQLQuery(q));

            } else if(q instanceof JDBCDeclaredQueryMetaData)
            {
               knownQueries.put(q.getMethod(), factory.createDeclaredSQLQuery(q));

            } else if(q instanceof JDBCQlQueryMetaData)
            {
               knownQueries.put(q.getMethod(), factory.createEJBQLQuery(q));
            }
         }
      }

      //
      // Automatic finders - The last resort
      //
      if(homeClass != null)
      {
         addAutomaticFinders(manager, homeClass.getMethods(), log);
      }

      if(localHomeClass != null)
      {
         addAutomaticFinders(manager, localHomeClass.getMethods(), log);
      }
   }

   public void clear()
   {
      this.knownQueries.clear();
   }

   private void addAutomaticFinders(
         JDBCStoreManager manager,
         Method[] homeMethods,
         Logger log)
   {

      JDBCCommandFactory factory = manager.getCommandFactory();
      JDBCEntityBridge entity = (JDBCEntityBridge) manager.getEntityBridge();
      for (int i = 0; i < homeMethods.length; i++)
      {
         Method method = homeMethods[i];

         if(!knownQueries.containsKey(method))
         {
            String name = method.getName();
            if(name.equals(FIND_ALL))
            {
               JDBCQueryMetaData q = new JDBCAutomaticQueryMetaData(
                     method,
                     entity.getMetaData().getReadAhead(),
                  entity.getMetaData().getQLCompiler(), false
                  );
               knownQueries.put(method, factory.createFindAllQuery(q));
            }
            else if(name.startsWith(FIND_BY) && !name.equals(FIND_BY_PK))
            {
               try
               {
                  JDBCQueryMetaData q = new JDBCAutomaticQueryMetaData(
                        method,
                        entity.getMetaData().getReadAhead(),
                     entity.getMetaData().getQLCompiler(), false);
                  knownQueries.put(method, factory.createFindByQuery(q));
               } catch (IllegalArgumentException e)
               {
                  log.debug("Could not create the finder " + name +
                        ", because no matching CMP field was found.");
               }
            }
         }
      }
   }
}
