/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.ejb3;

import java.io.DataInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.spi.PersistenceProvider;
import javax.security.jacc.PolicyConfiguration;
import javax.security.jacc.PolicyConfigurationFactory;
import org.hibernate.ejb.HibernatePersistence;
import org.hibernate.ejb.packaging.PersistenceMetadata;
import org.hibernate.ejb.packaging.PersistenceXmlLoader;
import org.jboss.aop.AspectManager;
import org.jboss.aop.DomainDefinition;
import org.jboss.ejb3.entity.InjectedEntityManager;
import org.jboss.ejb3.entity.InjectedEntityManagerFactory;
import org.jboss.ejb3.entity.ManagedEntityManagerFactory;
import org.jboss.ejb3.entity.PersistenceInfoImpl;
import org.jboss.ejb3.injection.DependsHandler;
import org.jboss.ejb3.mdb.ConsumerManager;
import org.jboss.ejb3.service.ServiceManager;
import org.jboss.ejb3.service.ServiceContainer;
import org.jboss.ejb3.remoting.RemoteProxyFactory;
import org.jboss.logging.Logger;
import org.jboss.naming.Util;
import org.jboss.util.file.ArchiveBrowser;
import org.jboss.util.file.ClassFileFilter;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ClassFile;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.StringMemberValue;

/**
 * An EjbModule represents a collection of beans that are deployed as a unit.
 *
 * @author <a href="mailto:bill@jboss.org">Bill Burke</a>
 * @version $Revision: 1.6.2.3 $
 */
public abstract class Ejb3Deployment implements EjbLinkResolver
{

   public static interface DeploymentUnit
   {
      ClassLoader getClassLoader();

      ClassLoader getResourceLoader();

      String getShortName();

      URL getUrl();

      URL getPersistenceXml();

      URL getEjbJarXml();

      URL getJbossXml();

      List<Class> getClasses();

      String getDefaultEntityManagerName();

      Map getDefaultPersistenceProperties();

      Hashtable getJndiProperties();

   }

   private static final Logger log = Logger.getLogger(Ejb3Deployment.class);
   public static final String INJECTED_ENTITY_MANAGER_CONTEXT = "java:/EntityManagers";
   public static final String INJECTED_ENTITY_MANAGER_FACTORY_CONTEXT = "java:/EntityManagerFactories";
   public static final String ACTUAL_ENTITY_MANAGER_FACTORY_CONTEXT = "java:/ActualEntityManagerFactories";
   public static final String MANAGED_ENTITY_FACTORY_CONTEXT = "java:/managedEntityFactories";

   protected DeploymentUnit di;

   protected LinkedHashMap containers = new LinkedHashMap();

   protected boolean hasEntities;
   protected List<String> explicitEntityClasses = new ArrayList<String>();

   protected ManagedEntityManagerFactory managedFactory;
   protected String managedFactoryJndiName;

   protected String defaultSLSBDomain = "Stateless Bean";
   protected String defaultSFSBDomain = "Stateful Bean";
   protected String defaultMDBDomain = "Message Driven Bean";
   protected String defaultServiceDomain = "Service Bean";
   protected InitialContext initialContext;

   // This is used to push the default client binding to the ProxyDeployer
   protected String defaultClientBinding = RemoteProxyFactory.DEFAULT_CLIENT_BINDING;


   public Ejb3Deployment(DeploymentUnit di)

   {
      this.di = di;
      try
      {
         initialContext = EJB3Util.getInitialContext(di.getJndiProperties());
      }
      catch (NamingException e)
      {
         throw new RuntimeException(e);
      }
   }

   public DeploymentUnit getDeploymentUnit()
   {
      return di;
   }

   public String getDefaultSLSBDomain()
   {
      return defaultSLSBDomain;
   }


   public String getDefaultClientBinding()
   {
      return defaultClientBinding;
   }

   /**
    * The default AOP domain for stateless session beans
    *
    *
    * @param defaultSLSBDomain
    */
   public void setDefaultSLSBDomain(String defaultSLSBDomain)
   {
      this.defaultSLSBDomain = defaultSLSBDomain;
   }

   public String getDefaultSFSBDomain()
   {
      return defaultSFSBDomain;
   }


   /**
    * The default stateful session bean aspect domain
    *
    * @param defaultSFSBDomain
    */
   public void setDefaultSFSBDomain(String defaultSFSBDomain)
   {
      this.defaultSFSBDomain = defaultSFSBDomain;
   }

   public String getDefaultMDBDomain()
   {
      return defaultMDBDomain;
   }

   /**
    * The default AOP domain for message driven beans.
    *
    * @param defaultMDBDomain
    */
   public void setDefaultMDBDomain(String defaultMDBDomain)
   {
      this.defaultMDBDomain = defaultMDBDomain;
   }

   public String getDefaultServiceDomain()
   {
      return defaultServiceDomain;
   }

   /**
    * default AOP domain for service beans.
    *
    * @param defaultServiceDomain
    */
   public void setDefaultServiceDomain(String defaultServiceDomain)
   {
      this.defaultServiceDomain = defaultServiceDomain;
   }

   protected String getJaccContextId()
   {
      return di.getShortName();
   }

   public Container getContainer(ObjectName name)
   {
      return (Container) containers.get(name);
   }

   public java.util.Map getContainers()
   {
      return containers;
   }

   public String getEjbJndiName(String ejbLink, Class businessIntf)
   {
      Iterator it = containers.values().iterator();
      while (it.hasNext())
      {
         Container container = (Container) it.next();
         if (container.getEjbName().equals(ejbLink))
         {
            return ProxyFactoryHelper.getJndiName(((EjbManagerMBean) container).getInstance(), businessIntf);
         }
      }
      return null;
   }

   protected void registerContainer(Container container, Collection depends)
           throws Exception
   {
      ObjectName on = container.getObjectName();
      containers.put(on, container);
      container.setEjbLinkResolver(this);
      log.debug("Bound ejb3 container " + on);
   }

   protected abstract PolicyConfiguration createPolicyConfiguration() throws Exception;

   protected abstract void initializeJacc(PolicyConfiguration pc, DeploymentUnit unit);

   public void create() throws Exception
   {
      long start = System.currentTimeMillis();

      PolicyConfiguration pc = createPolicyConfiguration();

      deploy();

      initializeManagedEntityManagerFactory();

      initializeJacc(pc, di);

      log.info("EJB3 deployment time took: "
               + (System.currentTimeMillis() - start));
   }

   protected void deploy() throws Exception
   {
      Ejb3HandlerFactory factory = Ejb3HandlerFactory.getInstance(this);
      if (di.getUrl() != null) deployUrl(factory);

      if (di.getClasses() != null)
      {
         for (Class explicit : di.getClasses())
         {
            if (explicit.isAnnotationPresent(Entity.class))
            {
               explicitEntityClasses.add(explicit.getName());
               hasEntities = true;
               continue;
            }
            String name = explicit.getName().replace('.', '/') + ".class";
            InputStream stream = explicit.getClassLoader().getResourceAsStream(name);
            deployElement(stream, factory, initialContext);
         }
      }
   }

   protected void deployUrl(Ejb3HandlerFactory factory)
           throws Exception
   {
      Iterator it = ArchiveBrowser.getBrowser(di.getUrl(), new ClassFileFilter());
      InitialContext ctx = initialContext;
      // need to look into every entry in the archive to see if anybody has tags
      // defined.
      while (it.hasNext())
      {
         InputStream stream = (InputStream) it.next();
         deployElement(stream, factory, ctx);
      }
   }

   protected void deployElement(InputStream stream, Ejb3HandlerFactory factory, InitialContext ctx)
           throws Exception
   {
      DataInputStream dstream = new DataInputStream(stream);
      ClassFile cf = null;
      try
      {
         cf = new ClassFile(dstream);
      }
      finally
      {
         dstream.close();
         stream.close();
      }
      deployElement(factory, cf, ctx);

   }

   protected void deployElement(Ejb3HandlerFactory factory, ClassFile cf, InitialContext ctx)
           throws Exception
   {
      Ejb3Handler handler = factory.createHandler(cf);
      handler.setCtxProperties(di.getJndiProperties());

      if (handler.isEjb())
      {
         List<Container> containers = handler.getContainers(cf, this);
         if (containers.size() > 0)
         {
            List<Collection> depends = handler.getAllDepends();
            for (int i = 0; i < containers.size(); ++i)
            {
               registerContainer(containers.get(i), depends.get(i));
            }
         }
      }
      else
      {
         AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.visibleTag);
         if (visible != null)
         {
            if (EJB3Util.isEntity(visible))
            {
               hasEntities = true;
               return;
            }
            Annotation info;
            if (EJB3Util.isService(visible))
            {
               log.info("found EJB3 service bean: " + cf.getName());
               info = visible.getAnnotation(org.jboss.annotation.ejb.Service.class
                       .getName());
               StringMemberValue mv = (StringMemberValue) info
                       .getMemberValue("name");
               String ejbName = cf.getName();
               if (mv != null)
                  ejbName = mv.getValue();
               Collection depends = DependsHandler.getAllDepends(cf, visible);

               String containerName = EJB3Util.getAspectDomain(visible, defaultServiceDomain);
               DomainDefinition def = AspectManager.instance().getContainer(containerName);
               if (def == null)
                  throw new RuntimeException("No container configured with name '" + containerName
                                             + "''");
               //TODO Use AOPManager interface once that has stabilized
               ServiceContainer container = new ServiceContainer(di.getClassLoader(), cf.getName(), ejbName, (AspectManager)def.getManager(),  di.getJndiProperties());
               container.setDefaultClientBinding(getDefaultClientBinding());
               Container manager = new ServiceManager(container, getJaccContextId());

               registerContainer(manager, depends);
            }
            else if (EJB3Util.isConsumer(visible))
            {
               log.info("found EJB3 consumer bean: " + cf.getName());
               info = visible.getAnnotation(org.jboss.annotation.ejb.Consumer.class
                       .getName());
               StringMemberValue mv = (StringMemberValue) info
                       .getMemberValue("name");
               String ejbName = cf.getName();
               if (mv != null)
                  ejbName = mv.getValue();
               Collection depends = DependsHandler.getAllDepends(cf, visible);
               String containerName = EJB3Util.getAspectDomain(visible, defaultMDBDomain);
               DomainDefinition def = AspectManager.instance().getContainer(containerName);
               if (def == null)
                  throw new RuntimeException("No container configured with name '" + containerName
                                             + "''");
               //TODO Use AOPManager interface once that has stabilized
               Container container = new ConsumerManager(di.getClassLoader(), cf
                       .getName(), ejbName, (AspectManager)def.getManager(), getJaccContextId(), di.getJndiProperties());
               registerContainer(container, depends);
            }

         }
      }
   }

   protected void initializeManagedEntityManagerFactory()
           throws Exception
   {
      URL propsUrl = null;
      propsUrl = di.getPersistenceXml();
      hasEntities = hasEntities || (propsUrl != null);
      if (!hasEntities && di.getUrl() != null)
      {
         if (di.getUrl().toString().endsWith(".par"))
         {
            hasEntities = true;
         }
         else
         {
            Iterator it = ArchiveBrowser.getBrowser(di.getUrl(), new ArchiveBrowser.Filter()
            {
               public boolean accept(String filename)
               {
                  return filename.endsWith(".hbm.xml");
               }
            });

            // need to look into every entry in the archive to see if anybody has tags
            // defined.
            while (it.hasNext())
            {
               hasEntities = true;
               break;
            }
         }

      }

      if (!hasEntities) return;

      Properties props = new Properties();
      props.putAll(di.getDefaultPersistenceProperties());
      props.put(HibernatePersistence.JACC_CONTEXT_ID, getJaccContextId());

      PersistenceInfoImpl pi = new PersistenceInfoImpl();
      pi.setClassLoader(di.getClassLoader());

      ArrayList<URL> jarFiles = new ArrayList<URL>();
      pi.setJarFiles(jarFiles);
      pi.setPersistenceProviderClassName(HibernatePersistence.class.getName());
      if (propsUrl != null)
      {
         PersistenceMetadata xml = null;
         log.info("Found persistence.xml file in EJB3 jar");
         xml = PersistenceXmlLoader.deploy(propsUrl);
         props.putAll(xml.getProps());
         pi.setEntityclassNames(xml.getClasses());
         pi.setEntityManagerName(xml.getName());
         pi.setPersistenceXmlFileUrl(propsUrl);
         pi.setMappingFileNames(xml.getMappingFiles());

         for (String jar : xml.getJarFiles())
         {
            URL url = null;
            try
            {
               url = new URL(jar);
            }
            catch (MalformedURLException e)
            {
               if (jar.startsWith(".."))
               {
                  if (di.getUrl() == null) throw new RuntimeException("relative <jar-file> not allowed when standalone deployment unit is used");
                  String base = di.getUrl().toString();
                  jar = jar.replaceAll("\\.\\./", "+");
                  int idx = jar.lastIndexOf('+');
                  jar = jar.substring(idx + 1);
                  for (int i = 0; i < idx + 1; i++)
                  {
                     int slash = base.lastIndexOf('/');
                     base = base.substring(0, slash + 1);
                  }
                  url = new URL(base + jar.substring(idx));
               }
               else
               {
                  File fp = new File(jar);
                  url = fp.toURL();
               }
            }
            log.info("adding JarFile URL: " + url.toString());
            jarFiles.add(url);
         }
         if (xml.getJtaDatasource() != null) props.setProperty("hibernate.connection.datasource", xml.getJtaDatasource());
         if (xml.getProvider() != null) pi.setPersistenceProviderClassName(xml.getProvider());
      }
      if (explicitEntityClasses.size() > 0)
      {
         List<String> classes = pi.getEntityclassNames();
         if (classes == null) classes = explicitEntityClasses;
         else classes.addAll(explicitEntityClasses);
         pi.setEntityclassNames(classes);
      }
      String jtaDatasource = props.getProperty("hibernate.connection.datasource");
      props.remove("hibernate.connection.datasource");
      pi.setJtaDataSource((javax.sql.DataSource) initialContext.lookup(jtaDatasource));
      pi.setProperties(props);

      // .par must come last in list.
      if (di.getUrl() != null) jarFiles.add(di.getUrl());


      if (pi.getEntityManagerName() == null)
      {
         String name = di.getDefaultEntityManagerName();
         pi.setEntityManagerName(name);
         log.info("default entity manager name: " + name);
      }
      managedFactoryJndiName = pi.getEntityManagerName();

      Class providerClass = Thread.currentThread().getContextClassLoader().loadClass(pi.getPersistenceProviderClassName());

      PersistenceProvider pp = (PersistenceProvider) providerClass.newInstance();
      EntityManagerFactory factory = pp.createContainerEntityManagerFactory(pi);
      Context actualCtx = Util.createSubcontext(initialContext, ACTUAL_ENTITY_MANAGER_FACTORY_CONTEXT);
      Context managersCtx = Util.createSubcontext(initialContext, INJECTED_ENTITY_MANAGER_CONTEXT);
      Context factoryCtx = Util.createSubcontext(initialContext, INJECTED_ENTITY_MANAGER_FACTORY_CONTEXT);
      Context managedFactoryCtx = Util.createSubcontext(initialContext, MANAGED_ENTITY_FACTORY_CONTEXT);

      if (managedFactoryJndiName != null)
      {
         log.info("Create EntityManager with JNDI name: " + managedFactoryJndiName);
         managedFactory = new ManagedEntityManagerFactory(factory, MANAGED_ENTITY_FACTORY_CONTEXT + "/" + managedFactoryJndiName);
         NonSerializableFactory.rebind(managedFactoryCtx, managedFactoryJndiName, managedFactory);
         NonSerializableFactory.rebind(actualCtx, managedFactoryJndiName, factory);
         EntityManager injectedManager = new InjectedEntityManager(managedFactory, di.getJndiProperties());
         NonSerializableFactory.rebind(managersCtx, managedFactoryJndiName, injectedManager);
         EntityManagerFactory injectedFactory = new InjectedEntityManagerFactory(factory, managedFactoryJndiName, di.getJndiProperties());
         NonSerializableFactory.rebind(factoryCtx, managedFactoryJndiName, injectedFactory);
      }
      else
      {
         managedFactory = new ManagedEntityManagerFactory(factory);
      }
   }

   public void start() throws Exception
   {
      Iterator it = containers.keySet().iterator();
      while (it.hasNext())
      {
         ObjectName on = (ObjectName) it.next();
         Container con = (Container) containers.get(on);
         con.setManagedEntityManagerFactory(managedFactory);
      }
   }

   public void stop() throws Exception
   {
      Context actualCtx = Util.createSubcontext(initialContext, ACTUAL_ENTITY_MANAGER_FACTORY_CONTEXT);
      Context managersCtx = Util.createSubcontext(initialContext, INJECTED_ENTITY_MANAGER_CONTEXT);
      Context factoryCtx = Util.createSubcontext(initialContext, INJECTED_ENTITY_MANAGER_FACTORY_CONTEXT);
      Context managedFactoryCtx = Util.createSubcontext(initialContext, MANAGED_ENTITY_FACTORY_CONTEXT);

      if (managedFactoryJndiName != null)
      {
         log.info("Create EntityManager with JNDI name: " + managedFactoryJndiName);
         NonSerializableFactory.unbind(managedFactoryCtx, managedFactoryJndiName);
         NonSerializableFactory.unbind(actualCtx, managedFactoryJndiName);
         NonSerializableFactory.unbind(managersCtx, managedFactoryJndiName);
         NonSerializableFactory.unbind(factoryCtx, managedFactoryJndiName);
      }

      if (managedFactory != null)
         managedFactory.destroy();
   }

   public void destroy() throws Exception
   {
      PolicyConfigurationFactory pcFactory = PolicyConfigurationFactory.getPolicyConfigurationFactory();
      PolicyConfiguration pc = pcFactory.getPolicyConfiguration(getJaccContextId(), true);
      pc.delete();
   }
}
