/*
 * 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.system;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.management.InstanceNotFoundException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanServer;
import javax.management.MBeanServerNotification;
import javax.management.MalformedObjectNameException;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationFilterSupport;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.jboss.logging.Logger;
import org.jboss.mx.server.ServerConstants;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * An abstract base class that provides for declarative JMX notification
 * subscription handling.
 * <p>
 * A JBoss service that is in addition a NotificationListener can
 * subclass ListenerServiceMBeanSupport instead of ServiceMBeanSupport
 * and specify at run-time, inline in the MBean descriptor using the
 * SubscriptionList attribute, the set of MBeans/notifications the
 * service wants to subscribe/receive.
 * <p>
 * Call subscribe(boolean dynamicSubscriptions) at anytime to register to
 * those MBeans and for those notifications that match the specified criteria.
 * Call unsubscribe() to unsubscribe for Notifications.
 * <p>
 * If true is passed to subscribe() the baseclass will monitor for
 * registration events from the MBeanServer and automatically subscribe
 * to new instances of MBeans that match the subscription criteria.
 * Monitoring for unsubscribe events in not necessary, since the MBeanServer
 * automatically removes subscriptions to unregistering MBeans.
 * <p>
 * An alternative subscribe(boolean dynamicSubscription, ObjectName listener)
 * can be used to specify a different MBean as the receiver of the
 * subscribed notifications. The specified MBean must be a NotificationListener.
 * <p>
 * To handle the incoming notifications override the handleNotification2()
 * method. The usual handleNotification() method should not be overriden,
 * since it is used to monitor the incoming notifications for registration
 * events coming from the MBeanServer, before delegating to
 * handleNotification2(), in order to implement dynamic subscriptions.
 *
 * @see ListenerServiceMBean
 * @see NotificationFilterFactory
 *
 * REVISIONS
 * =========
 * 14/03/05, dimitris
 * The filter mechanism has been extended to support specification
 * of arbitrary filters, using dynamic filter factory plugins
 * implementing the NotificationFilterFactory interface.
 * Three filter factories corresponding to the "standard" jmx
 * notification filters are supplied by default in package
 * org.jboss.system.filterfactory.
 * 
 * 19/10/04, dimitris
 * renamed inner class MBeanInfo to SubscriptionInfo and made public,
 * using NotificationFilter instead of NotificationFilterSupport and added new
 * subscribe(List subscriptionList, boolean dynamicSubscriptions, ObjectName listener)
 * to allow external programmatic specification of the subscription list.
 *  
 * 28/02/04, dimitris
 * explicit subscribe()/unsubscribe() replaced implicit start()/stop();
 * dynamic subscription behaviour can be enabled/disabled, plus it is
 * now possible to specify an external MBean as notification listener.
 * 
 * 02/02/04, dimitris
 * Initial version, that resulted by generalizing the notification
 * subscription mechanism of the snmp adapter.
 * 
 * @author <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
 *
 * @version $Revision: 1.1.2.9 $
**/
public abstract class ListenerServiceMBeanSupport
   extends ServiceMBeanSupport
   implements ListenerServiceMBean, NotificationListener
{
   // Private Data --------------------------------------------------
     
   /** The mbean subscription specification list */
   private List sublist; // if null, subscriptions not made
   
   /** The mbean subscription config in XML form */
   private Element xmllist; // set through SubscriptionList attribute
   
   /** monitoring and registering to new MBeans, as they appear */
   private boolean dynamicSubscriptions;
   
   /** the receiver of the notifications */
   private ObjectName listener;
   
   /** Handback to identify our own MBeanServerDelegate subscription */
   private Object myHandback;  
   
   /** Filter to receive only registration events */
   private NotificationFilterSupport myFilter;   
  
   // Constructors -------------------------------------------------
    
   /**
    * Constructs a <tt>ListenerServiceMBeanSupport</tt>.
   **/
   public ListenerServiceMBeanSupport()
   {
        super();
        init();
   }

   /**
    * Constructs a <tt>ListenerServiceMBeanSupport</tt>.
    *
    * Pass-through to ServiceMBeanSupport.
    *
    * @param type   The class type to determine Logger name from.
   **/
   public ListenerServiceMBeanSupport(final Class type)
   {
      super(type);
      init();
   }
   
   /**
    * Constructs a <tt>ListenerServiceMBeanSupport</tt>.
    *
    * Pass-through to ServiceMBeanSupport.
    *
    * @param category   The logger category name.
   **/
   public ListenerServiceMBeanSupport(final String category)
   {
      super(category);
      init();
   }

   /**
    * Constructs a <tt>ListenerServiceMBeanSupport</tt>.
    *
    * Pass-through to ServiceMBeanSupport.
    *
    * @param log   The logger to use.
   **/
   public ListenerServiceMBeanSupport(final Logger log)
   {
      super(log);
      init();
   }    
    
   // ListenerServiceMBean Implementation ---------------------------
    
   /**
    * Used to configure the JMX notification subscriptions.
    *
    * The configuration is done inline in the mbean descriptor.
    *
    * See jboss-subscription.dtd
   **/
   public void setSubscriptionList(Element list)
   {
      // deep copy the provided Element for later use
      // not sure if really necessary - play it safe
      this.xmllist = (Element)list.cloneNode(true);
   }
   
   // Public API ----------------------------------------------------

   /**
    * Subscribes this MBean for JMX notifications.
    *
    * @param dynamicSubscriptions indicates whether to monitor and subscribe
    *                             to new MBeans that match the specification.
   **/
   public void subscribe(boolean dynamicSubscriptions)
      throws Exception
   {
      subscribe(dynamicSubscriptions, this.getServiceName());
   }
   
   /**
    * Subscribes a listener MBean for JMX notifications.
    *
    * @param dynamicSubscriptions indicates whether to monitor and subscribe
    *                             to new MBeans that match the specification.
    * @param listener the receiver of the notifications.
   **/
   public void subscribe(boolean dynamicSubscriptions, ObjectName listener)
      throws Exception
   {
      // we need an xml subscription specification
      if (this.xmllist != null)
      {
         // Parse the XML spec
         log.debug("Parsing subscription specification");
         List subscriptionList = parseXMLSubscriptionSpec(this.xmllist);
         
         subscribe(subscriptionList, dynamicSubscriptions, listener);
      }
      else
         log.debug("Subscription specification not provided");      
   }
   
   /**
    * Subscribes a listener MBean for JMX notifications.
    *
    * @param subscriptionList the list containing SubscriptionInfo data.
    * @param dynamicSubscriptions indicates whether to monitor and subscribe
    *                             to new MBeans that match the specification.
    * @param listener the receiver of the notifications.
   **/
   public void subscribe(List subscriptionList, boolean dynamicSubscriptions, ObjectName listener)
      throws Exception
   {
      // return if already subscribed
      if (this.sublist != null)
         return;

      // we need an subscription specification
      if (subscriptionList != null)
      {
         // store input  
         this.sublist = subscriptionList;         
         this.dynamicSubscriptions = dynamicSubscriptions;
         this.listener = listener;         
         
         if (log.isDebugEnabled())
            log.debug(this.sublist);
            
         log.debug("Subscribing for JMX notifications" +
                  ", dynamic=" + dynamicSubscriptions +
                  (this.getServiceName().equals(listener) ? "" :
                  ", listener='" + listener + "'"));
                  
         bulkRegister();
      
         if (dynamicSubscriptions == true)
         {
            // Subscribe to MBeanServerDelegate MBean for registrations
            getServer().addNotificationListener(
               new ObjectName(ServerConstants.MBEAN_SERVER_DELEGATE),
               this.getServiceName(),
               this.myFilter,
               this.myHandback
            );
            
            log.debug("Subscribed to MBeanServerDelegate, too");
         }
      }
      else
         log.debug("Subscription list not provided");
   }

   /**
    * Unsubscribes for JMX notifications
   **/
   public void unsubscribe()
   {
      // return if not subscribed
      if (this.sublist == null)
         return;
      
      log.debug("Removing all JMX notification subscriptions");
      bulkUnregister();
      
      if (this.dynamicSubscriptions == true)
      {
         // Unbscribe from MBeanServerDelegate MBean for registrations
         try {
            getServer().removeNotificationListener(
               new ObjectName(ServerConstants.MBEAN_SERVER_DELEGATE),
               this.getServiceName(),
               this.myFilter,
               this.myHandback
            );             
         
            log.debug("Unsubscribed from MBeanServerDelegate, too");
         }
         catch (MalformedObjectNameException e) 
         {
            // shouldn't happen!
            log.warn("Could not convert '" + ServerConstants.MBEAN_SERVER_DELEGATE
                   + "' to ObjectName", e);
         }
         catch (InstanceNotFoundException e) 
         {
            // shouldn't happen
            log.warn("Could not unsubscribe from non-existent MBeanServerDelegate!", e);
         }
         catch (ListenerNotFoundException e) 
         {
            // shouldn't happend
            log.warn("Could not unsubscribe from MBeanServerDelegate", e);
         }
      }
      // use sublist to indicate we've unsubscribed
      this.sublist = null;
   }
   
   // NotificationListener -----------------------------------------
    
   /**
    * DO NOT OVERRIDE THIS!
    *
    * Handles dynamic subscriptions before delegating to
    * handleNotification2()
   **/
   public void handleNotification(Notification notification, Object handback)
   {
      // check if the notification is for me!
      if (this.dynamicSubscriptions == true && handback == this.myHandback) 
      {
         if (log.isTraceEnabled())
            log.trace("It's for me: " + notification + ", handback:" + handback);
         
         String type = notification.getType();
         ObjectName target = null;
         try {
            target = ((MBeanServerNotification)notification).getMBeanName();
         }
         catch (ClassCastException e) {
            log.warn("MBeanServer sent unknown notification class type: " +
                     notification.getClass().getName());
            return;
         }
               
         if (type.equals(MBeanServerNotification.REGISTRATION_NOTIFICATION)) 
         {
            // iterate over the subscription specification
            Iterator i = this.sublist.iterator();
      
            while (i.hasNext()) 
            {
               SubscriptionInfo mbeanInfo = (SubscriptionInfo)i.next();
         
               ObjectName objectName = mbeanInfo.getObjectName();
                  
               try 
               {
                  if(objectName.apply(target)) 
                  {
                     if (log.isDebugEnabled())
                        log.debug("ObjectName: '" + target + "' matched '"
                                + objectName + "'");
                     
                     // go for it!
                     singleRegister(
                        this.getServer(),
                        target,
                        this.listener,
                        mbeanInfo.getFilter(),
                        mbeanInfo.getHandback()
                     );
                  }
               }
               catch (Exception e) 
               {
                  // catch exceptions from apply()
                  // shouldn't happen
                  log.warn("Caught exception from ObjectName.apply("
                         + target + ")", e);
               }
            }
         }
         else 
         {
            log.warn("Got unknown notification type from MBeanServerDelegate: "
                   + type);
         }
      }
      else // delegate to subclass
         handleNotification2(notification, handback);
   }

   /**
    * Override to add notification handling!
   **/
   public void handleNotification2(Notification notification, Object handback)
   {
      // empty!
   }
   
   // Private Methods -----------------------------------------------

   /**
    * Initialises myself
   **/
   private void init()
   {
      // just pickup a unique object
      this.myHandback = new Integer(Integer.MAX_VALUE);
      
      // allow only registration events
      this.myFilter = new NotificationFilterSupport();
      this.myFilter.enableType(MBeanServerNotification.REGISTRATION_NOTIFICATION);
   }

   /**
    * Subscribes for notifications to a single MBean
   **/
   private void singleRegister(
      MBeanServer server, ObjectName target, ObjectName listener,
      NotificationFilter filter, Object handback)
   {
      try
      {
         server.addNotificationListener(target, listener, filter, handback);
               
         logSubscription(target, listener, handback, filter);
      }
      catch (InstanceNotFoundException e) 
      {
         // ignore - mbean might not be registered
         if (log.isDebugEnabled())
            log.debug("Could not subscribe to: '" + target
                    + "', target or listener MBean not registered");
      }
      catch (RuntimeException e) 
      {
         log.warn("Failed to subscribe to: '" + target 
                + "', maybe not a notification broadcaster or: '" + listener
                + "', maybe not a notification listener");
      }      
   }

   /**
    * Unsubscribes for notifications from a single MBean 
   **/
   private void singleUnregister(
      MBeanServer server, ObjectName target, ObjectName listener,
      NotificationFilter filter, Object handback)
   {
      try
      {
         // remove the matching subscription
         server.removeNotificationListener(target, listener, filter, handback);
         
         if (log.isDebugEnabled())
            log.debug("Unsubscribed from: '" + target + "'");
      }
      catch (InstanceNotFoundException e) 
      {
         // ignore - target mbean not present
         if (log.isDebugEnabled())
            log.debug("Could not unsubscribe from non-existent: '"
                    + target + "'");
      }
      catch (ListenerNotFoundException e) 
      {
         // May happen if target is not a notification broadcaster
         // and so we hadn't registered in the first place
         if (log.isDebugEnabled())
            log.debug("Could not unsubscribe from: '" + target + "'");
      }
      catch (RuntimeException e) 
      {
         // whatever
         if (log.isDebugEnabled())
            log.debug("Could not unsubscribe from: '" + target + "'");
      }          
   }
   
   /**
    * Performs the notification subscriptions
   **/
   private void bulkRegister()
   {
      // iterate over the subscription specification
      Iterator i = this.sublist.iterator();
      
      // find out my server
      MBeanServer server = this.getServer();
      
      while (i.hasNext()) 
      {
         SubscriptionInfo mbeanInfo = (SubscriptionInfo)i.next();
         
         ObjectName objectName = mbeanInfo.getObjectName();
         Object handback = mbeanInfo.getHandback();
         NotificationFilter filter = mbeanInfo.getFilter();
         
         if (objectName.isPattern()) 
         {
            Set mset = server.queryNames(objectName, null);
            
            if (log.isDebugEnabled())
               log.debug("ObjectName: '" + objectName + "' matched "
                        + mset.size() + " MBean(s)");
               
            Iterator j = mset.iterator();
            while (j.hasNext())
               singleRegister(server, (ObjectName)j.next(), this.listener,
                              filter, handback);
         }
         else
            singleRegister(server, objectName, this.listener, filter, handback);
      }
   }
   
   /**
    * Performs bulk unregistration
   **/
   private void bulkUnregister()
   {
      // iterate over the subscription specification
      Iterator i = this.sublist.iterator();
      
      // find out my server
      MBeanServer server = this.getServer();
      
      while (i.hasNext()) 
      {
         SubscriptionInfo mbeanInfo = (SubscriptionInfo)i.next();
         
         ObjectName objectName = mbeanInfo.getObjectName();
         Object handback = mbeanInfo.getHandback();
         NotificationFilter filter = mbeanInfo.getFilter();
         
         if (objectName.isPattern()) 
         {
            Set mset = server.queryNames(objectName, null);
            
            if (log.isDebugEnabled())
               log.debug("ObjectName: '" + objectName + "' matched "
                       + mset.size() + " MBean(s)");
               
            Iterator j = mset.iterator();
            while (j.hasNext())
               singleUnregister(server, (ObjectName)j.next(), this.listener,
                                filter, handback);
         }
         else
            singleUnregister(server, objectName, this.listener, filter, handback);
      }      
   }
   
   /**
    * Logs subscription info
   **/
   private void logSubscription(
      ObjectName objectName, ObjectName listener,
      Object handback, NotificationFilter filter)
   {
      if (log.isDebugEnabled()) 
      {
         StringBuffer sbuf = new StringBuffer(100);
         
         sbuf.append("Subscribed to: { objectName='").append(objectName);
         sbuf.append("', listener='").append(listener);
         sbuf.append("', handback=").append(handback);
         sbuf.append(", filter=");
         sbuf.append(filter == null ? null : filter.toString());
         sbuf.append(" }");
         
         log.debug(sbuf.toString());
      }
   }
   
   /**
    * Encapsulte the factory and filter creation logic
    */
   private NotificationFilter createNotificationFilter(String factoryClass, Element filterConfig)
      throws Exception
   {
      NotificationFilterFactory factory;
      try
      {
         // try to load the factory Class
         Class clazz = Thread.currentThread().getContextClassLoader().loadClass(factoryClass);
         factory = (NotificationFilterFactory)clazz.newInstance();
      }
      catch (Exception e) // ClassNotFoundException, IllegalAccessException, InstantiationException
      {
         // factory class not found. Make a second try using
         // the 'org.jboss.system.filterfactory.' package prefix
         // for the "standard" filter factories provided with jboss.
         // If that fails, too, rethrow the original exception.
         try
         {
            factoryClass = "org.jboss.system.filterfactory." + factoryClass;
            Class clazz = Thread.currentThread().getContextClassLoader().loadClass(factoryClass);
            factory = (NotificationFilterFactory)clazz.newInstance();
         }
         catch (Exception inner)
         {
            throw e;
         }
      }
      // delegate the filter creation/configuration to the factory
      return factory.createNotificationFilter(filterConfig);
   }
   
   /**
    * Parses the XML subscription specification
   **/
   private ArrayList parseXMLSubscriptionSpec(Element root)
       throws Exception
   {
      ArrayList slist = new ArrayList();
      
      // parse level 0 - subscription-list
      if (!root.getNodeName().equals(SL_ROOT_ELEMENT))
      {
         throw new Exception("Expected '" + SL_ROOT_ELEMENT + "' element, "
                    + "got: " + "'" + root.getNodeName() + "'");
      }
      else
      {
         NodeList rootlist = root.getChildNodes();
            
         for (int i = 0; i < rootlist.getLength(); i++) 
         {
            // Parse level 1 - look for mbeans
            Node mbean = rootlist.item(i);
                
            if (mbean.getNodeName().equals(SL_MBEAN_ELEMENT)) 
            {
               // mbean found look for name & handback attrs
               String name = null;
               
               if (((Element)mbean).hasAttribute(SL_MBEAN_NAME_ATTRIBUTE))
               {
                  name = ((Element)mbean).getAttribute(SL_MBEAN_NAME_ATTRIBUTE);
               }
               else
               {
                  throw new Exception("'" + SL_MBEAN_ELEMENT + "' element must have a '" 
                                    + SL_MBEAN_NAME_ATTRIBUTE + "' attribute");
               }

               String handback = null;
               if (((Element)mbean).hasAttribute(SL_MBEAN_HANDBACK_ATTRIBUTE))
               {
                  handback = ((Element)mbean).getAttribute(SL_MBEAN_HANDBACK_ATTRIBUTE);
               }
               
               // try to convert name to the correct data type
               // may throw MalformedObjectNameException
               ObjectName objectName = new ObjectName(name);
               
               // Parse level 2 - see if we have a filter for this subscription
               NotificationFilter filter = null;
               
               NodeList mbeanChildren = mbean.getChildNodes();

               // check for filter spec, as a single mbean child node               
               for (int j = 0; j < mbeanChildren.getLength(); j++)
               {
                  Node mbeanChildNode = mbeanChildren.item(j);
                  
                  // check if this is a 'filter' node
                  if (mbeanChildNode.getNodeName().equals(SL_FILTER_ELEMENT))
                  {
                     // look for the 'factory' attribute
                     String factory = null;
                     if (((Element)mbeanChildNode).hasAttribute(SL_FILTER_FACTORY_ATTRIBUTE))
                     {
                        factory = ((Element)mbeanChildNode).getAttribute(SL_FILTER_FACTORY_ATTRIBUTE);
                        
                        // instantiate the factory and request the filter
                        filter = createNotificationFilter(factory, (Element)mbeanChildNode);
                        break;
                     }
                     else
                     {
                        throw new Exception("'" + SL_FILTER_ELEMENT + "' element must have a '"
                              + SL_FILTER_FACTORY_ATTRIBUTE + "' attribute");
                     }
                  }
               }
               
               if (filter == null)
               {
                  // if no filter has been set check for old-style
                  // <notification type="..."/> construct that results
                  // in a fixed NotificationFilterSupport filter

                  // need to find out all notification types (if any)
                  // in order to create the Notification filter
                  ArrayList tmplist = new ArrayList(mbeanChildren.getLength());
               
                  for (int j = 0; j < mbeanChildren.getLength(); j++) 
                  {
                     Node mbeanChildNode = mbeanChildren.item(j);
                  
                     // check if this is a 'notification' element
                     if (mbeanChildNode.getNodeName().equals(SL_NOTIFICATION_ELEMENT)) 
                     {
                        // look for 'type' attribute
                        String type = null;
                        if (((Element)mbeanChildNode).hasAttribute(SL_NOTIFICATION_TYPE_ATTRIBUTE)) 
                        {
                           type = ((Element)mbeanChildNode).getAttribute(SL_NOTIFICATION_TYPE_ATTRIBUTE);
                           tmplist.add(type);
                        }
                        else
                        {
                           throw new Exception("'" + SL_NOTIFICATION_ELEMENT + "' element must have a '"
                                 + SL_NOTIFICATION_TYPE_ATTRIBUTE + "' attribute");
                        }
                     }
                  }
                  // create the filter (if needed)
                  if (tmplist.size() > 0) 
                  {
                     NotificationFilterSupport sfilter = new NotificationFilterSupport();
                     for (int j = 0; j < tmplist.size(); j++)
                     {
                        sfilter.enableType((String)tmplist.get(j));
                     }
                     filter = sfilter;
                  }                  
               }
               slist.add(new SubscriptionInfo(objectName, handback, filter));
            }
         }
      }
      return slist;
   }
    
   // Inner Class ---------------------------------------------------
   
   /**
    * Inner data holder class to store the parsed subscription specification.
   **/
   public static final class SubscriptionInfo
   {
      // Private Data -----------------------------------------------
      
      /** MBean notification broadcaster or pattern */
      private ObjectName objectName;
      
      /** Optional handback object to identify a subscription */
      private Object handback;
      
      /** Arbitrary NotificationFilter */
      private NotificationFilter filter;
      
      // Constructor ------------------------------------------------
      
      /**
       * Simple CTOR
      **/
      public SubscriptionInfo(ObjectName objectName, Object handback, NotificationFilter filter)
      {
         this.objectName = objectName;
         this.handback = handback;
         this.filter = filter;
      }
      
      // Accessors --------------------------------------------------
      /**
       * Gets objectname
      **/
      public ObjectName getObjectName()
      {
         return this.objectName;
      }
      
      /**
       * Gets handback object
      **/
      public Object getHandback()
      {
         return this.handback;
      }
      
      /**
       * Gets notification filter
      **/
      public NotificationFilter getFilter()
      {
         return this.filter;
      }
      
      /**
       * Pretty prints
      **/
      public String toString()
      {
         StringBuffer sbuf = new StringBuffer(100);
            
         sbuf.append("SubscriptionInfo { objectName='").append(this.objectName);
         sbuf.append("', handback=").append(this.handback);
         sbuf.append(", filter=");
         sbuf.append(this.filter == null ? null : this.filter.toString());
         
         sbuf.append(" }");
         
         return sbuf.toString();
      }
   }
}    
