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

import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.Attribute;
import javax.management.MBeanInfo;

import org.jboss.deployment.DeploymentInfo;
import org.jboss.deployment.SubDeployerSupport;
import org.jboss.deployment.DeploymentException;
import org.jboss.system.ServiceControllerMBean;
import org.jboss.mx.util.MBeanProxyExt;
import org.jboss.mx.util.ObjectNameFactory;
import org.jboss.hibernate.jmx.Hibernate;

/**
 * Deployer for Hibernate <tt>har</tt> archives. A Hibernate archive
 * is expected to have a .har extension and include:<ul>
 * <li> <tt>hibernate-service.xml</tt>
 * <li> persistent classes
 * <li> <tt>hbm.xml</tt> mapping documents.</ul>
 *
 * @jmx:mbean
 *      name="jboss.har:service=HARDeployer"
 *      extends="org.jboss.deployment.SubDeployerMBean"
 *
 * @author <a href="mailto:alex@jboss.org">Alexey Loubyansky</a>
 * @author <a href="mailto:steve@hibernate.org">Steve Ebersole</a>
 * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
 *
 * @version <tt>$Revision: 1.2.2.4 $</tt>
 */
public class HARDeployer extends SubDeployerSupport
   implements HARDeployerMBean
{
   private static final ObjectName OBJECT_NAME = ObjectNameFactory.create("jboss.har:service=HARDeployer");
   private static final String DESCRIPTOR_NAME = "hibernate-service.xml";
   private static final String RELATIVE_DESCRIPTOR_URL = "META-INF/" + DESCRIPTOR_NAME;

   private ServiceControllerMBean serviceController;

   /** A map of current deployments. */
   private HashMap deployments = new HashMap();

   /** A flag indicating if deployment descriptors should be validated */
   private boolean validateDTDs;

   /**
    * Default CTOR used to set default values to the Suffixes and RelativeOrder
    * attributes. Those are read at subdeployer registration time by the MainDeployer
    * to alter its SuffixOrder.
    */
   public HARDeployer()
   {
      setSuffixes( new String[] { ".har" } );
      setRelativeOrder( RELATIVE_ORDER_400 );
   }  
   
   /**
    * Returns the deployed applications.
    *
    * @jmx:managed-operation
    */
   public Iterator getDeployedApplications()
   {
      return deployments.values().iterator();
   }

   protected ObjectName getObjectName(MBeanServer server, ObjectName name)
      throws MalformedObjectNameException
   {
      return name == null ? OBJECT_NAME : name;
   }

   /**
    * Get a reference to the ServiceController
    */
   protected void startService() throws Exception
   {
      serviceController = ( ServiceControllerMBean ) MBeanProxyExt.create(
              ServiceControllerMBean.class,
               ServiceControllerMBean.OBJECT_NAME, server
      );

      // register with MainDeployer
      super.startService();
   }

   /**
    * Implements the template method in superclass. This method stops all the
    * applications in this server.
    */
   protected void stopService() throws Exception
   {
      Iterator modules = deployments.values().iterator();
      while ( modules.hasNext() )
      {
         DeploymentInfo di = (DeploymentInfo)modules.next();
         stop(di);
      }

      // avoid concurrent modification exception
      modules = new ArrayList( deployments.values() ).iterator();
      while ( modules.hasNext() )
      {
         DeploymentInfo di = (DeploymentInfo)modules.next();
         destroy(di);
      }
      deployments.clear();

      // deregister with MainDeployer
      super.stopService();

      serviceController = null;
   }

   /**
    * Get the flag indicating that ejb-jar.dtd, jboss.dtd &amp;
    * jboss-web.dtd conforming documents should be validated
    * against the DTD.
    *
    * @jmx:managed-attribute
    */
   public boolean getValidateDTDs()
   {
      return validateDTDs;
   }

   /**
    * Set the flag indicating that ejb-jar.dtd, jboss.dtd &amp;
    * jboss-web.dtd conforming documents should be validated
    * against the DTD.
    *
    * @jmx:managed-attribute
    */
   public void setValidateDTDs(boolean validate)
   {
      this.validateDTDs = validate;
   }

   // -------------------------------------------------------------------------
   // Sub-deployable bit ------------------------------------------------------
   //    This is all essentially saying that the hibernate-service.xml file
   //    consitutes a nested deployment (i.e., isDeployable) such that a
   //    subdeployment is generated for that file by itself.  The whole
   //    purpose of this fact is to get SARDeployer to pick up the service
   //    xml file and parse it.  At some point this gets "linked back" to our
   //    DeploymentInfo.subDeployments so that we have access to it.
   //
   // TODO : this all seems very hacky.
   //    Would prefer having SARDeployer xml parsing logic encapsulated so that it
   //    can just get used directly from here.
   protected void processNestedDeployments(DeploymentInfo di) throws DeploymentException
   {
      super.processNestedDeployments( di );
   }

   protected void deployUrl(DeploymentInfo di, URL url, String name) throws DeploymentException
   {
      super.deployUrl( di, url, name );
   }

   public boolean isDeployable(String name, URL url)
   {
      // Allow nested jars to get deployed.
      //
      // We also mark the 'hibernate-service.xml' file as deployable so that
      // the SARDeployer can pick it up and parse its content as described above; yick!
      log.debug( "Checking deployability of [name=" + name + ", url=" + url.getFile() + "]" );
      return name.endsWith( ".jar" ) || name.endsWith( RELATIVE_DESCRIPTOR_URL );
   }
   // -------------------------------------------------------------------------

   /**
    * The HARDeployer accepts either archives ending in '.har' or exploded
    * directories (with name ending in '.har/').  Furthermore, the deployment
    * must contain a file named 'META-INF/hibernate-service.xml' in order to
    * be accepted.
    *
    * @param di The deployment info for the deployment to be checked for
    * acceptance.
    * @return True if the conditions mentioned above hold true; false otherwise.
    */
   public boolean accepts(DeploymentInfo di)
   {
      // To be accepted the deployment's root name must end in .har
      String urlStr = di.url.getFile();
      if ( !urlStr.endsWith( ".har" ) && !urlStr.endsWith( ".har/" ) )
      {
         return false;
      }

      // However the har must also contain a ${RELATIVE_DESCRIPTOR_URL}
      boolean accept = false;
      try
      {
         URL dd = di.localCl.findResource( RELATIVE_DESCRIPTOR_URL );
         if ( dd != null )
         {
            // If the DD url is not a subset of the urlStr then this is coming
            // from a jar referenced by the deployment jar manifest and the
            // this deployment jar it should not be treated as an ejb-jar
            if ( di.localUrl != null )
            {
               urlStr = di.localUrl.toString();
            }

            String ddStr = dd.toString();
            if( ddStr.indexOf( urlStr ) >= 0 )
            {
               accept = true;
            }
         }
      }
      catch( Exception ignore )
      {
      }

      log.debug( "accept> url=" + di.url + ", accepted=" + accept );

      return accept;
   }

   /**
    * Initialize the given deployment.  Overriden to perform custom watch logic.
    *
    * @param di The deployment to be initialized.
    * @throws DeploymentException
    */
   public void init(DeploymentInfo di) throws DeploymentException
   {
      log.debug( "Deploying HAR; init; " + di );
      try
      {
         if( "file".equalsIgnoreCase( di.url.getProtocol() ) )
         {
            File file = new File( di.url.getFile() );

            if ( !file.isDirectory() )
            {
               // If not directory we watch the package
               di.watch = di.url;
            }
            else
            {
               // If directory we watch the xml files
               di.watch = new URL( di.url, DESCRIPTOR_NAME );
            }
         }
         else
         {
            // We watch the top only, no directory support
            di.watch = di.url;
         }
      }
      catch( Exception e )
      {
         if ( e instanceof DeploymentException )
         {
            throw ( DeploymentException ) e;
         }
         throw new DeploymentException( "failed to initialize", e );
      }

      // invoke super-class initialization
      super.init( di );
   }

   public synchronized void create(DeploymentInfo di) throws DeploymentException
   {
      log.debug( "Deploying HAR; create; " + di );
      super.create( di );

      // Impl note: this, again, is relying on the fact that the SARDeployer has
      // already parsed the hibernate-service.xml file and created any defined
      // mbeans; these are then available through the 'subDeployments' attribute
      // of the DeploymentInfo related to the overall har file.  clear as mud? :)
      //
      // *there should only ever be one sub-deployment here...
      Iterator subdeployments = di.subDeployments.iterator();
      while ( subdeployments.hasNext() )
      {
         DeploymentInfo nested = ( DeploymentInfo ) subdeployments.next();
         log.debug( "Checking sub-deployment [" + nested.url + "] for descriptor-name" );

         // check each sub-deployment to find the one relating to our
         // hibernate-service.xml file.
         if ( nested.url.getFile().endsWith( DESCRIPTOR_NAME ) )
         {
            log.debug( "Attempting to locate HibernateMBean in sub-deployment [" + nested.url + "]" );
            Iterator mbeans = nested.mbeans.iterator();

            // For all the mbeans defined on our service descriptor, locate the
            // Hibernate mbean and pass it the har deployment's url so that
            // it can auto detect the mapping files.
            while ( mbeans.hasNext() )
            {
               ObjectName service = ( ObjectName ) mbeans.next();
               log.debug( "Testing [" + service + "] as HibernateMBean" );
               if ( isHibernateMBean( service ) )
               {
                  log.debug( "Located HibernateMBean" );
                  Attribute attr = new Attribute( "HarUrl", di.url );
                  try
                  {
                     server.setAttribute( service, attr );
                  }
                  catch( Exception e )
                  {
                     throw new DeploymentException( "Failed to set HarUrl attribute: " + e.getMessage(), e );
                  }
               }
            }
            break;
         }
      }
   }

   public synchronized void start(DeploymentInfo di) throws DeploymentException
   {
      log.debug( "Deploying HAR; start; " + di );
      super.start( di );
   }

   public void stop(DeploymentInfo di) throws DeploymentException
   {
      log.debug( "Undeploying HAR; stop; " + di );
      try
      {
         serviceController.stop(di.deployedObject);
      }
      catch(Exception e)
      {
         throw new DeploymentException("problem stopping har module: " + di.url, e);
      }
      super.stop(di);
   }

   public void destroy(DeploymentInfo di) throws DeploymentException
   {
      log.debug( "Undeploying HAR; destroy; " + di );
      // FIXME: If the put() is obsolete above, this is obsolete, too
      deployments.remove(di.url);

      try
      {
         serviceController.destroy(di.deployedObject);
         serviceController.remove(di.deployedObject);
      }
      catch(Exception e)
      {
         throw new DeploymentException("problem destroying har module: " + di.url, e);
      }
      super.destroy( di );
   }

   private boolean isHibernateMBean(ObjectName service)
   {
      try
      {
         MBeanInfo serviceInfo = server.getMBeanInfo( service );
         return Hibernate.class.getName().equals( serviceInfo.getClassName() );
      }
      catch( Throwable t )
      {
         log.warn( "Unable to determine whether MBean [" + service + "] is Hibernate MBean" );
         return false;
      }
   }
}