/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, 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.system;

import javax.management.AttributeChangeNotification;
import javax.management.JMException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;

import org.jboss.deployment.DeploymentInfo;
import org.jboss.deployment.SARDeployerMBean;
import org.jboss.logging.Logger;
import org.jboss.mx.util.JBossNotificationBroadcasterSupport;

/**
 * An abstract base class JBoss services can subclass to implement a
 * service that conforms to the ServiceMBean interface. Subclasses must
 * override {@link #getName} method and should override 
 * {@link #startService}, and {@link #stopService} as approriate.
 *
 * @see ServiceMBean
 * 
 * @author <a href="mailto:rickard.oberg@telkel.com">Rickard Öberg</a>
 * @author Scott.Stark@jboss.org
 * @author <a href="mailto:andreas@jboss.org">Andreas Schaefer</a>
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @version $Revision: 57205 $
 */
public class ServiceMBeanSupport
   extends JBossNotificationBroadcasterSupport
   implements ServiceMBean, MBeanRegistration
{
   /** The signature for service controller operations */
   public static final String[] SERVICE_CONTROLLER_SIG = new String[] { ObjectName.class.getName() };
   
   /**
    * The instance logger for the service.  Not using a class logger
    * because we want to dynamically obtain the logger name from
    * concreate sub-classes.
    */
   protected Logger log;
   
   /** The MBeanServer which we have been register with. */
   protected MBeanServer server;

   /** The object name which we are registsred under. */
   protected ObjectName serviceName;

   /** The current state this service is in. */
   private int state = UNREGISTERED;

   /** For backwards compatibility */
   private boolean isJBossInternalLifecycleExposed = false;
   
   /**
    * Construct a <t>ServiceMBeanSupport</tt>.
    *
    * <p>Sets up logging.
    */
   public ServiceMBeanSupport()
   {
      // can not call this(Class) because we need to call getClass()
      this.log = Logger.getLogger(getClass().getName());
      log.trace("Constructing");
   }

   /**
    * Construct a <t>ServiceMBeanSupport</tt>.
    *
    * <p>Sets up logging.
    *
    * @param type   The class type to determine category name from.
    */
   public ServiceMBeanSupport(final Class type)
   {
      this(type.getName());
   }
   
   /**
    * Construct a <t>ServiceMBeanSupport</tt>.
    *
    * <p>Sets up logging.
    *
    * @param category   The logger category name.
    */
   public ServiceMBeanSupport(final String category)
   {
      this(Logger.getLogger(category));
   }

   /**
    * Construct a <t>ServiceMBeanSupport</tt>.
    *
    * @param log   The logger to use.
    */
   public ServiceMBeanSupport(final Logger log)
   {
      this.log = log;
      log.trace("Constructing");
   }

   /**
    * Use the short class name as the default for the service name.
    */
   public String getName()
   {
      // TODO: Check if this gets called often, if so cache this or remove if not used
      // use the logger so we can better be used as a delegate instead of sub-class
      return org.jboss.util.Classes.stripPackageName(log.getName());
   }
   
   public ObjectName getServiceName()
   {
      return serviceName;
   }

   /**
    * Provide access to the service DeploymentInfo. This is only available
    * after the service has passed its create step.
    * 
    * @return The service DeploymentInfo if found registered under the SARDeployer.
    * @throws JMException - thrown on failure to invoke
    *    SARDeployer.getService(ObjectName)
    */ 
   public DeploymentInfo getDeploymentInfo()
      throws JMException
   {
      Object[] args = {serviceName};
      String[] sig = {serviceName.getClass().getName()};
      DeploymentInfo sdi = (DeploymentInfo) server.invoke(SARDeployerMBean.OBJECT_NAME,
         "getService", args, sig);
      return sdi;
   }

   public MBeanServer getServer()
   {
      return server;
   }
   
   public int getState()
   {
      return state;
   }
   
   public String getStateString()
   {
      return states[state];
   }
   
   public Logger getLog()
   {
      return log;
   }


   ///////////////////////////////////////////////////////////////////////////
   //                             State Mutators                            //
   ///////////////////////////////////////////////////////////////////////////
   
   public void create() throws Exception
   {
      if (serviceName != null && isJBossInternalLifecycleExposed)
         server.invoke(ServiceController.OBJECT_NAME, "create", new Object[] { serviceName }, SERVICE_CONTROLLER_SIG);
      else
         jbossInternalCreate();
   }
   
   public void start() throws Exception
   {
      if (serviceName != null && isJBossInternalLifecycleExposed)
         server.invoke(ServiceController.OBJECT_NAME, "start", new Object[] { serviceName }, SERVICE_CONTROLLER_SIG);
      else
         jbossInternalStart();
   }
   
   public void stop()
   {
      try
      {
         if (serviceName != null && isJBossInternalLifecycleExposed)
            server.invoke(ServiceController.OBJECT_NAME, "stop", new Object[] { serviceName }, SERVICE_CONTROLLER_SIG);
         else
            jbossInternalStop();
      }
      catch (Throwable t)
      {
         log.warn("Error in stop " + jbossInternalDescription(), t);
      }
   }
   
   public void destroy()
   {
      try
      {
         if (serviceName != null && isJBossInternalLifecycleExposed)
            server.invoke(ServiceController.OBJECT_NAME, "destroy", new Object[] { serviceName }, SERVICE_CONTROLLER_SIG);
         else
            jbossInternalDestroy();
      }
      catch (Throwable t)
      {
         log.warn("Error in destroy " + jbossInternalDescription(), t);
      }
   }
   
   protected String jbossInternalDescription()
   {
      if (serviceName != null)
         return serviceName.toString();
      else
         return getName();
   }
   
   public void jbossInternalLifecycle(String method) throws Exception
   {
      if (method == null)
         throw new IllegalArgumentException("Null method name");
      
      if (method.equals("create"))
         jbossInternalCreate();
      else if (method.equals("start"))
         jbossInternalStart();
      else if (method.equals("stop"))
         jbossInternalStop();
      else if (method.equals("destroy"))
         jbossInternalDestroy();
      else
         throw new IllegalArgumentException("Unknown lifecyle method " + method);
   }
   
   protected void jbossInternalCreate() throws Exception
   {
      log.debug("Creating " + jbossInternalDescription());
      
      try
      {
         createService();
         state = CREATED;
      }
      catch (Exception e)
      {
         log.debug("Initialization failed " + jbossInternalDescription(), e);
         throw e;
      }
      
      log.debug("Created " + jbossInternalDescription());
   }

   protected void jbossInternalStart() throws Exception
   {
      if (state == STARTING || state == STARTED || state == STOPPING)
         return;

      if (state != CREATED && state != STOPPED && state != FAILED)
      {
         log.debug("Start requested before create, calling create now");         
         create();
      }
      
      state = STARTING;
      sendStateChangeNotification(STOPPED, STARTING, getName() + " starting", null);
      log.debug("Starting " + jbossInternalDescription());

      try
      {
         startService();
      }
      catch (Exception e)
      {
         state = FAILED;
         sendStateChangeNotification(STARTING, FAILED, getName() + " failed", e);
         log.debug("Starting failed " + jbossInternalDescription(), e);
         throw e;
      }

      state = STARTED;
      sendStateChangeNotification(STARTING, STARTED, getName() + " started", null);
      log.debug("Started " + jbossInternalDescription());
   }
   
   protected void jbossInternalStop()
   {
      if (state != STARTED)
         return;
      
      state = STOPPING;
      sendStateChangeNotification(STARTED, STOPPING, getName() + " stopping", null);
      log.debug("Stopping " + jbossInternalDescription());

      try
      {
         stopService();
      }
      catch (Throwable e)
      {
         state = FAILED;
         sendStateChangeNotification(STOPPING, FAILED, getName() + " failed", e);
         log.warn("Stopping failed " + jbossInternalDescription(), e);
         return;
      }
      
      state = STOPPED;
      sendStateChangeNotification(STOPPING, STOPPED, getName() + " stopped", null);
      log.debug("Stopped " + jbossInternalDescription());
   }

   protected void jbossInternalDestroy()
   {
      if (state == DESTROYED)
         return;
      
      if (state == STARTED)
      {
         log.debug("Destroy requested before stop, calling stop now");
         stop();
      }
      
      log.debug("Destroying " + jbossInternalDescription());
      
      try
      {
         destroyService();
      }
      catch (Throwable t)
      {
         log.warn("Destroying failed " + jbossInternalDescription(), t);
      }
      state = DESTROYED;
      log.debug("Destroyed " + jbossInternalDescription());
   }


   ///////////////////////////////////////////////////////////////////////////
   //                                JMX Hooks                              //
   ///////////////////////////////////////////////////////////////////////////
   
   /**
    * Callback method of {@link MBeanRegistration}
    * before the MBean is registered at the JMX Agent.
    * 
    * <p>
    * <b>Attention</b>: Always call this method when you overwrite it in a subclass
    *                   because it saves the Object Name of the MBean.
    *
    * @param server    Reference to the JMX Agent this MBean is registered on
    * @param name      Name specified by the creator of the MBean. Note that you can
    *                  overwrite it when the given ObjectName is null otherwise the
    *                  change is discarded (maybe a bug in JMX-RI).
    */
   public ObjectName preRegister(MBeanServer server, ObjectName name)
      throws Exception
   {
      this.server = server;

      serviceName = getObjectName(server, name);
      
      return serviceName;
   }
   
   public void postRegister(Boolean registrationDone)
   {
      if (!registrationDone.booleanValue())
      {
         log.info( "Registration is not done -> stop" );
         stop();
      }
      else
      {
         state = REGISTERED;
         // This is for backwards compatibility - see whether jbossInternalLifecycle is exposed
         try
         {
            MBeanInfo info = server.getMBeanInfo(serviceName);
            MBeanOperationInfo[] ops = info.getOperations();
            for (int i = 0; i < ops.length; ++i)
            {
               if (ops[i] != null && ServiceController.JBOSS_INTERNAL_LIFECYCLE.equals(ops[i].getName()))
               {
                  isJBossInternalLifecycleExposed = true;
                  break;
               }
            }
         }
         catch (Throwable t)
         {
            log.warn("Unexcepted error accessing MBeanInfo for " + serviceName, t);
         }
      }
   }

   public void preDeregister() throws Exception
   {
   }
   
   public void postDeregister()
   {
      //clean up
      try
      {
         if (serviceName != null && isJBossInternalLifecycleExposed)
            server.invoke(ServiceController.OBJECT_NAME, "remove", new Object[] { serviceName }, SERVICE_CONTROLLER_SIG);
      }
      catch (Throwable t)
      {
         log.warn("Unexpected error during removal. " + serviceName, t);
      }

      server = null;
      serviceName = null;
      state = UNREGISTERED;
   }

   /**
    * The <code>getNextNotificationSequenceNumber</code> method returns 
    * the next sequence number for use in notifications.
    *
    * @return a <code>long</code> value
    */
   protected long getNextNotificationSequenceNumber()
   {
      return nextNotificationSequenceNumber();
   }


   ///////////////////////////////////////////////////////////////////////////
   //                       Concrete Service Overrides                      //
   ///////////////////////////////////////////////////////////////////////////

   /**
    * Sub-classes should override this method if they only need to set their
    * object name during MBean pre-registration.
    */
   protected ObjectName getObjectName(MBeanServer server, ObjectName name)
      throws MalformedObjectNameException
   {
      return name;
   }

   /**
    * Sub-classes should override this method to provide
    * custum 'create' logic.
    *
    * <p>This method is empty, and is provided for convenience
    *    when concrete service classes do not need to perform
    *    anything specific for this state change.
    */
   protected void createService() throws Exception {}
   
   /**
    * Sub-classes should override this method to provide
    * custum 'start' logic.
    * 
    * <p>This method is empty, and is provided for convenience
    *    when concrete service classes do not need to perform
    *    anything specific for this state change.
    */
   protected void startService() throws Exception {}
   
   /**
    * Sub-classes should override this method to provide
    * custum 'stop' logic.
    * 
    * <p>This method is empty, and is provided for convenience
    *    when concrete service classes do not need to perform
    *    anything specific for this state change.
    */
   protected void stopService() throws Exception {}
   
   /**
    * Sub-classes should override this method to provide
    * custum 'destroy' logic.
    * 
    * <p>This method is empty, and is provided for convenience
    *    when concrete service classes do not need to perform
    *    anything specific for this state change.
    */
   protected void destroyService() throws Exception {}
   
   // Private -------------------------------------------------------
   
   /**
    * Helper for sending out state change notifications
    */
   private void sendStateChangeNotification(int oldState, int newState, String msg, Throwable t)
   {
      long now = System.currentTimeMillis();
      
      AttributeChangeNotification stateChangeNotification = new AttributeChangeNotification(
         this,
         getNextNotificationSequenceNumber(), now, msg,
         "State", "java.lang.Integer",
         new Integer(oldState), new Integer(newState)
         );
      stateChangeNotification.setUserData(t);
      
      sendNotification(stateChangeNotification);      
   }
}
