/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2009, 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.exoplatform.container.mc.impl;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.jboss.beans.metadata.plugins.AbstractBeanMetaData;
import org.jboss.beans.metadata.spi.BeanMetaData;
import org.jboss.kernel.spi.deployment.KernelDeployment;
import org.jboss.xb.binding.Unmarshaller;
import org.jboss.xb.binding.UnmarshallerFactory;
import org.jboss.xb.binding.resolver.MutableSchemaResolver;
import org.jboss.xb.binding.sunday.unmarshalling.SingletonSchemaResolverFactory;
import org.picocontainer.ComponentAdapter;

import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Configuration reader and holder for conf/mc-int-config.xml
 *
 * mc-int-config.xml uses jboss bean deployer syntax (the one used in jboss-beans.xml).
 * It is only interested in annotation and injection configuration though, and doesn't treat <bean> element attributes
 * as referring to mc bean declarations - it treats them as references to exo-kernel components.
 * <p>
 * Example:
 * <pre>
 * <deployment xmlns="urn:jboss:bean-deployer:2.0">
 * <bean class="org.exoplatform.container.definition.PortalContainerConfig">
 *    <annotation>@org.exoplatform.container.mc.impl.InterceptMC</annotation>
 * </bean>
 * <bean name="InjectingBean2">
 *    <annotation>@org.exoplatform.container.mc.impl.InterceptMC(injectionMode=
 *    org.exoplatform.container.mc.impl.MCInjectionMode.STANDARD)</annotation>
 * </bean>
 * </pre>
 * <p>
 * In the above example the first <em>bean</em> declaration refers to exo-kernel configured service that
 * is returned by exo container when calling getComponentInstanceOfType(clazz).
 *
 * The second <em>bean</em> declaration refers to exo-kernel configured service that is returned by exo
 * container when calling getComponentInstance(name).
 *
 * This configuration makes it possible to activate injection outside of you classes. When annotations are declared both
 * inside the class, and in mc-int-config.xml, the latter completely overrides the former.
 * It is also possible to use mc-int-config.xml to nullify an existing annotation in your service class.
 *
 * <em>Warning:</em> Make sure there are no leading or trailing white spaces in the body of <em>annotation</em> element.
 *
 * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
 */
public class MCIntConfig
{
   /**
    * Logger.
    */
   private static final Log log = ExoLogger.getLogger("exo.kernel.mc-int.MCIntConfig");

   /**
    * Configuration for <em>beans</em> specified with <em>name</em>.
    */
   private Map<String, DeploymentData> confByKey = new HashMap<String, DeploymentData>();

   /**
    * Configuration for <em>beans</em> specified with <em>class</em>.
    */
   private Map<String, DeploymentData> confByBean = new HashMap<String, DeploymentData>();

   /**
    * The only constructor.
    */
   MCIntConfig()
   {
      Enumeration<URL> urls = null;
      try
      {
         urls = Thread.currentThread().getContextClassLoader().getResources("conf/mc-int-config.xml");
      }
      catch (IOException e)
      {
         throw new RuntimeException("Failed to get resources: conf/mc-int-config.xml", e);
      }

      UnmarshallerFactory factory = UnmarshallerFactory.newInstance();
      MutableSchemaResolver resolver = SingletonSchemaResolverFactory.getInstance().getSchemaBindingResolver();

      while (urls.hasMoreElements())
      {
         URL url = urls.nextElement();

         KernelDeployment deployment = parseConfigURL(factory, resolver, url);

         List<BeanMetaData> beans = deployment.getBeans();
         for (BeanMetaData mdata : beans)
         {
            String name = mdata.getName();
            DeploymentData data = confByKey.get(name);
            if (data == null)
            {
               String bean = mdata.getBean();
               data = confByBean.get(bean);
            }
            if (data != null)
            {
               log.warn("Overriding existing mc-int-beans configuration for bean: " + data.data
                     + " from " + data.deployment.getName() + " with one from " + deployment.getName());
            }
            addConf(new DeploymentData(deployment, mdata));
         }
      }
   }

   /**
    * Retrieve configuration by component key.
    *
    * @param key component key
    * @return configuration for specific component
    */
   public AbstractBeanMetaData getByKey(String key)
   {
      DeploymentData dd = confByKey.get(key);
      if (dd == null)
      {
         return null;
      }
      return (AbstractBeanMetaData) dd.data;
   }

   /**
    * Retrieve configuration by component type.
    *
    * @param bean component type
    * @return configuration for specific component
    */
   public AbstractBeanMetaData getByBean(String bean)
   {
      DeploymentData dd = confByBean.get(bean);
      if (dd == null)
      {
         return null;
      }
      return (AbstractBeanMetaData) dd.data;
   }

   /**
    * Retrieve configuration for specific component adapter.
    * First lookup by key, then by type.
    *
    * @param adapter component adapter
    * @return configuration for specific component
    */
   public AbstractBeanMetaData getByAdapter(ComponentAdapter adapter)
   {
      Object key = adapter.getComponentKey();
      String strKey = key instanceof Class ? ((Class) key).getName() : String.valueOf(key);
      BeanMetaData ret = getByKey(strKey);
      if (ret != null)
      {
         return (AbstractBeanMetaData) ret;
      }

      ret = getByBean(strKey);
      return (AbstractBeanMetaData) ret;
   }

   /**
    * Add parsed configuration for one component.
    *
    * @param deploymentData confiuration for specific component
    */
   private void addConf(DeploymentData deploymentData)
   {
      String name = deploymentData.data.getName();
      if (name != null)
      {
         confByKey.put(name, deploymentData);
      }
      String bean = deploymentData.data.getBean();
      if (bean != null)
      {
         confByBean.put(bean, deploymentData);
      }
      if (name == null && bean == null)
      {
         throw new RuntimeException("Configuration error: bean found with no name and no class in "
               + deploymentData.deployment.getName());
      }
   }

   /**
    * Parse the configuration.
    *
    * @param factory JBoss XB unmarshaller factory
    * @param resolver JBoss XB schema resolver
    * @param url Url pointing to mc-int-config.xml file
    * @return JBoss XB bean representing a parsed configuration
    */
   private KernelDeployment parseConfigURL(UnmarshallerFactory factory, MutableSchemaResolver resolver, URL url)
   {
      final boolean trace = log.isTraceEnabled();
      if (trace)
      {
         log.trace("Parsing " + url);
      }
      long start = System.currentTimeMillis();

      Unmarshaller unmarshaller = factory.newUnmarshaller();
      KernelDeployment deployment = null;
      try
      {
         deployment = (KernelDeployment) unmarshaller.unmarshal(url.toString(), resolver);
      }
      catch (Exception e)
      {
         throw new RuntimeException("Failed to parse xml " + url, e);
      }

      if (deployment == null)
      {
         throw new RuntimeException("The xml " + url + " is not well formed!");
      }
      deployment.setName(url.toString());

      if (trace)
      {
         long now = System.currentTimeMillis();
         log.trace("Parsing " + url + " took " + (now - start) + " milliseconds");
      }

      return deployment;
   }

   /**
    * Data structure that holds component configuration - deployment configuration association.
    */
   static class DeploymentData
   {
      KernelDeployment deployment;
      BeanMetaData data;

      public DeploymentData(KernelDeployment deployment, BeanMetaData data)
      {
         this.deployment = deployment;
         this.data = data;
      }
   }
}
