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

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.beans.IntrospectionException;
import java.beans.PropertyEditorManager;
import java.beans.PropertyEditor;
import java.beans.PropertyDescriptor;
import java.beans.Introspector;
import java.beans.BeanInfo;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import javax.management.Attribute;
import javax.management.AttributeChangeNotification;
import javax.management.AttributeChangeNotificationFilter;
import javax.management.Descriptor;
import javax.management.InstanceNotFoundException;
import javax.management.JMException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanServer;
import javax.management.Notification;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.RuntimeOperationsException;
import javax.management.RuntimeErrorException;
import javax.management.MBeanInfo;
import javax.management.modelmbean.InvalidTargetObjectTypeException;
import javax.management.modelmbean.ModelMBean;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.modelmbean.ModelMBeanInfoSupport;
import javax.management.modelmbean.ModelMBeanOperationInfo;

import org.jboss.logging.Logger;
import org.jboss.mx.interceptor.Interceptor;
import org.jboss.mx.interceptor.ModelMBeanAttributeInterceptor;
import org.jboss.mx.interceptor.ModelMBeanInfoInterceptor;
import org.jboss.mx.interceptor.PersistenceInterceptor;
import org.jboss.mx.interceptor.ModelMBeanInterceptor;
import org.jboss.mx.interceptor.ObjectReferenceInterceptor;
import org.jboss.mx.interceptor.PersistenceInterceptor2;
import org.jboss.mx.interceptor.NullInterceptor;
import org.jboss.mx.persistence.NullPersistence;
import org.jboss.mx.persistence.PersistenceManager;
import org.jboss.mx.server.AbstractMBeanInvoker;
import org.jboss.mx.server.InvocationContext;
import org.jboss.mx.server.MBeanInvoker;
import org.jboss.mx.util.JBossNotificationBroadcasterSupport;


/**
 * An extension of the {@link org.jboss.mx.server.MBeanInvoker MBeanInvoker}
 * that implements the base Model MBean functionality, essentially making the
 * Model MBean just another invoker of managed resources.
 *
 * @see javax.management.modelmbean.ModelMBean
 * @see org.jboss.mx.server.MBeanInvoker
 *
 * @author  <a href="mailto:juha@jboss.org">Juha Lindfors</a>.
 * @author Matt Munz
 * @author  <a href="mailto:dimitris@jboss.org">Dimitris Andreadis</a>
 * @version $Revision: 1.1.4.24 $
 */
public abstract class ModelMBeanInvoker extends AbstractMBeanInvoker
   implements ModelMBean, ModelMBeanConstants
{
   Logger log = Logger.getLogger(ModelMBeanInvoker.class.getName());

   // Attributes ----------------------------------------------------

   /**
    * The resource type string of the managed resource, such as
    * {@link ModelMBeanConstants#OBJECT_REF} or
    * {@link XMBeanConstants#STANDARD_INTERFACE}. This type string can be
    * used by the invoker to determine the behavior implemented by the
    * invocation chain and how the managed resource is exposed to the client
    * programs.
    */
   protected String resourceType              = null;

   /**
    * Persistence manager.
    */
   protected PersistenceManager persistence   = new NullPersistence();

   /**
    * Notification broadcaster for this Model MBean.
    */
   protected JBossNotificationBroadcasterSupport notifier = new JBossNotificationBroadcasterSupport();

   /**
    * Notification sequence number for generic Model MBean notifications.
    */
   protected long notifierSequence     = 1;

   /**
    * Notification sequence number for attribute change notifications.
    */
   protected long attrNotifierSequence = 1;


   // Constructors --------------------------------------------------

   /**
    * Default constructor.
    */
   public ModelMBeanInvoker()
   {
      super();
   }

   /**
    * Creates a Model MBean instance and initializes it with the given
    * Model MBean metadata.
    *
    * @param   info  Model MBean metadata
    */
   public ModelMBeanInvoker(ModelMBeanInfo info) throws MBeanException
   {
      this();

      setModelMBeanInfo(info);
   }



   // ModelMBean implementation -------------------------------------

   /**
    * Sets the MBean metadata for this Model MBean instance.
    *
    * @param   info  Model MBean metadata
    */
   public void setModelMBeanInfo(ModelMBeanInfo info) throws MBeanException, RuntimeOperationsException
   {
      if (info == null)
         throw new IllegalArgumentException("MBeanInfo cannot be null.");

      // need to type to an instance of MBeanInfo -- therefore the extra copy here
      this.info = new ModelMBeanInfoSupport(info);

      // Apply the MBeanInfo injection if requested
      ModelMBeanInfo minfo = (ModelMBeanInfo) info;
      Descriptor mbeanDescriptor = null;
      try
      {
         mbeanDescriptor = minfo.getDescriptor("",
                        ModelMBeanConstants.MBEAN_DESCRIPTOR);
      }
      catch (MBeanException e)
      {
         log.warn("Failed to obtain descriptor: "+ModelMBeanConstants.MBEAN_DESCRIPTOR, e);
         return;
      }

      String type = (String) mbeanDescriptor.getFieldValue(
         ModelMBeanConstants.MBEAN_INFO_INJECTION_TYPE);
      if( type != null )
      {
         inject(ModelMBeanConstants.MBEAN_INFO_INJECTION_TYPE,
            type, MBeanInfo.class, info);
      }
   }

   /**
    * Sets the managed resource for this Model MBean instance. The resource
    * type must be known to the Model MBean implementation (see
    * {@link #isSupportedResourceType} for more information).
    *
    * @param   ref            reference to the managed resource
    * @param   resourceType   resource type identification string
    */
   public void setManagedResource(Object ref, String resourceType)
      throws MBeanException, InstanceNotFoundException, InvalidTargetObjectTypeException
   {
      if (ref == null)
         throw new IllegalArgumentException("Resource reference cannot be null.");

      if (!isSupportedResourceType(ref, resourceType))
         throw new InvalidTargetObjectTypeException("Unsupported resource type: " + resourceType);

      setResource(ref);
      this.resourceType = resourceType;
   }

   // ModelMBeanNotificationBroadcaster implementation --------------

   public void addNotificationListener(NotificationListener listener,
                                       NotificationFilter filter,
                                       Object handback)
   {
      notifier.addNotificationListener(listener, filter, handback);
   }

   public void removeNotificationListener(NotificationListener listener)
      throws ListenerNotFoundException
   {
      notifier.removeNotificationListener(listener);
   }

   public void removeNotificationListener(NotificationListener listener,
                                          NotificationFilter filter,
                                          Object handback)
      throws ListenerNotFoundException
   {
      notifier.removeNotificationListener(listener, filter, handback);
   }

   /**
    * Sends a notification with a given string message. The notification
    * type will be set as
    * {@link ModelMBeanConstants#GENERIC_MODELMBEAN_NOTIFICATION GENERIC_MODELMBEAN_NOTIFICATION}.
    *
    * @param   message  notification message
    */
   public void sendNotification(String message) throws MBeanException
   {
      Notification notif = new Notification(
         GENERIC_MODELMBEAN_NOTIFICATION, // type
         this,                            // source
         ++notifierSequence,              // sequence number
         message                          // message
         );

      sendNotification(notif);
   }

   /**
    * Sends a notification.
    *
    * @param   notification notification to send
    */
   public void sendNotification(Notification notification) throws MBeanException
   {
      notifier.sendNotification(notification);
   }

   /**
    * Sends an attribute change notification.
    *
    * @param   notification attribute change notification to send
    */
   public void sendAttributeChangeNotification(AttributeChangeNotification notification)
      throws MBeanException
   {
      notifier.sendNotification(notification);
   }

   /**
    * Sends an attribute change notification.
    *
    * @param   oldValue attribute with the old value
    * @param   newValue attribute with the new value
    */
   public void sendAttributeChangeNotification(Attribute oldValue, Attribute newValue)
      throws MBeanException
   {
      if (!(oldValue.getName().equals(newValue.getName())))
         throw new IllegalArgumentException("attribute name mismatch between oldvalue and newvalue");

      String attr = oldValue.getName();
      String type = ((ModelMBeanInfo)info).getAttribute(attr).getType();

      AttributeChangeNotification notif = new AttributeChangeNotification(
         this,                          // source
         ++attrNotifierSequence,        // seq. #
         System.currentTimeMillis(),    // time stamp
         "" + attr + " changed from " + oldValue + " to " + newValue,
         attr, type,                    // name & type
         oldValue.getValue(),
         newValue.getValue()            // values
         );

      notifier.sendNotification(notif);
   }

   public MBeanNotificationInfo[] getNotificationInfo()
   {
      return info.getNotifications();
   }

   /**
    * @deprecated use {@link #addNotificationListener} instead
    */
   public void addAttributeChangeNotificationListener(
      NotificationListener listener,
      String attributeName,
      Object handback) throws MBeanException
   {
      AttributeChangeNotificationFilter filter = new AttributeChangeNotificationFilter();
      filter.enableAttribute(attributeName);
      notifier.addNotificationListener(listener, filter, handback);
   }

   /**
    * @deprecated use {@link #removeNotificationListener} instead
    */
   public void removeAttributeChangeNotificationListener(
      NotificationListener listener,
      String attributeName) throws MBeanException, ListenerNotFoundException
   {
      notifier.removeNotificationListener(listener);
   }

   // PersistentMBean implementation --------------------------------
   public void load() throws MBeanException, InstanceNotFoundException
   {
      if (info == null)
         return;

      persistence.load(this, info);
   }

   public void store() throws MBeanException, InstanceNotFoundException
   {
      persistence.store(info);
   }


   // MBeanRegistration implementation ------------------------------

   /**
    * The default implementation of <tt>preRegister</tt> invokes the
    * {@link #configureInterceptorStack} method which sets up the interceptors
    * for this Model MBean instance. Subclasses may override the
    * <tt>configureInterceptorStack()</tt> method to implement their own
    * interceptor stack configurations. See the JavaDoc for
    * <tt>configureInterceptorStack()</tt> for more information.    <p>
    *
    * After the interceptor configuration, this implementation invokes the
    * {@link #load} method on this Model MBean instance. This will attempt
    * to load a pre-existing management attribute state for this Model MBean
    * instance. See the Javadoc for <tt>load()</tt> for more information.
    */
   public ObjectName invokePreRegister(MBeanServer server, ObjectName name)
      throws Exception
   {
      // Check for null metadata and prevent registration if metadata
      // has not been set
      if (info == null)
      {
         throw new RuntimeErrorException(
               new Error("MBeanInfo has not been set."));
      }
      
      // initialize MBeanInfo first
      ModelMBeanInfo minfo = (ModelMBeanInfo) info;
      ModelMBeanInfoSupport infoSupport = (ModelMBeanInfoSupport) info;

      // add the resource classname to the MBean info
      Descriptor mbeanDescriptor = infoSupport.getMBeanDescriptor();
      mbeanDescriptor.setField(
         ModelMBeanConstants.RESOURCE_CLASS,
         getResource().getClass().getName()
      );

      // Set the mbean descriptor on the info context for use by interceptor config
      this.getMBeanInfoCtx.setDescriptor(mbeanDescriptor);

      configureInterceptorStack(minfo, server, name);

      //Set initial values provided in descriptors
      setValuesFromMBeanInfo();

      initPersistence(server, name);

      //Set (and override) values from mbean persistence store.
      load();

      return super.invokePreRegister(server, name);
   }

   // Protected ---------------------------------------------------

   /**
    * initializes the persistence manager based on the info for this bean.
    * If this is successful, loads the bean from the persistence store.
    */
   protected void initPersistence(MBeanServer server, ObjectName name)
         throws MBeanException, InstanceNotFoundException
   {
      Descriptor[] descriptors;
      ModelMBeanInfo minfo = (ModelMBeanInfo) getMetaData();
      
      try
      {
         descriptors = minfo.getDescriptors(MBEAN_DESCRIPTOR);
      }
      catch (MBeanException e)
      {
         log.error("Failed to obtain MBEAN_DESCRIPTORs", e);
         return;
      }

      if (descriptors == null)
      {
         return;
      }
      String persistMgrName = null;
      for (int i = 0; ((i < descriptors.length) && (persistMgrName == null)); i++)
      {
         persistMgrName = (String) descriptors[i].getFieldValue(PERSISTENCE_MANAGER);
      }
      if (persistMgrName == null)
      {
         log.debug("No "+PERSISTENCE_MANAGER
            +" descriptor found, null persistence will be used");
         return;
      }

      try
      {
         persistence = (PersistenceManager) server.instantiate(persistMgrName);
         log.debug("Loaded persistence mgr: "+persistMgrName);

         // Add the ObjectName to the ModelMBean Descriptor
         // so that it can be used by the PersistentManager (if needed)
         Descriptor descriptor = minfo.getMBeanDescriptor();
         descriptor.setField(ModelMBeanConstants.OBJECT_NAME, name);
         minfo.setMBeanDescriptor(descriptor);
      }
      catch (Exception cause)
      {
         log.error("Unable to instantiate the persistence manager:"
            + persistMgrName, cause);
      }
   }

   protected void initOperationContexts(MBeanOperationInfo[] operations)
   {
      // make sure we invoke the super class initialization sequence first
      super.initOperationContexts(operations);

      for (int i = 0; i < operations.length; ++i)
      {
         OperationKey key = new OperationKey(operations[i]);

         InvocationContext ctx = (InvocationContext)operationContextMap.get(key);
         ModelMBeanOperationInfo info = (ModelMBeanOperationInfo) operations[i];
         ctx.setDescriptor(info.getDescriptor());
      }
   }

   protected void initAttributeContexts(MBeanAttributeInfo[] attributes)
   {
      super.initAttributeContexts(attributes);

      for (int i = 0; i < attributes.length; ++i)
      {
         ModelMBeanAttributeInfo info = (ModelMBeanAttributeInfo) attributes[i];
         String name = info.getName();
         InvocationContext ctx = (InvocationContext)attributeContextMap.get(name);
         ctx.setDescriptor(info.getDescriptor());
         ctx.setWritable(info.isWritable());
      }
   }

   /** Build the getMBeanInfo, operation, and attribute interceptor stacks
    * and associated these with the corresponding InvocationContexts.
    * 
    * @param info - the ModelMBean metadata
    * @param server - the MBeanServer the ModelMBean is registering with
    * @param name - the ModelMBean name
    * @throws Exception
    */ 
   protected void configureInterceptorStack(ModelMBeanInfo info,
      MBeanServer server, ObjectName name)
      throws Exception
   {
      /* Get the MBeanInfo accessor interceptor stack. This is the interceptor
      stack declared at the model mbean level. In 3.2.3 and earlier this was
      the interceptor stack for all operation and attribute access so we
      use this as the default interceptor stack
      */
      List defaultInterceptors = getInterceptors(getMBeanInfoCtx.getDescriptor());
      List interceptors = null;
      if( defaultInterceptors != null )
         interceptors = new ArrayList(defaultInterceptors);
      if (interceptors == null)
      {
         // Set the default interceptor stack
         interceptors = getMBeanInfoCtx.getInterceptors();
      }
      /* We always add the ModelMBeanInfoInterceptor as we expect that
      users are specifying additional interceptors, not overriding the
      source of the ModelMBeanInfo.
      */
      String mbeanName = name != null ? name.toString() : info.getClassName();
      interceptors.add(new ModelMBeanInfoInterceptor(mbeanName));
      getMBeanInfoCtx.setInterceptors(interceptors);

      // Get any custom interceptors specified at the attribute level
      for(Iterator it = attributeContextMap.entrySet().iterator(); it.hasNext();)
      {
         Map.Entry entry = (Map.Entry)it.next();

         InvocationContext ctx = (InvocationContext)entry.getValue();
         List list = getInterceptors(ctx.getDescriptor());
         if (list == null)
         {
            // Use the mbean inteceptors if sepecified
            if( defaultInterceptors != null )
               list = new ArrayList(defaultInterceptors);
            else
               list = new ArrayList();
         }
         // Add the attribute accessor semantic interceptors
         list.add(new PersistenceInterceptor());
         list.add(new ModelMBeanAttributeInterceptor());
         ctx.setInterceptors(list);
      }

      // Get any custom interceptors specified at the operation level
      for(Iterator it = operationContextMap.entrySet().iterator(); it.hasNext();)
      {
         Map.Entry entry = (Map.Entry)it.next();

         InvocationContext ctx = (InvocationContext)entry.getValue();
         List list = getInterceptors(ctx.getDescriptor());
         if( list == null && defaultInterceptors != null )
            list = new ArrayList(defaultInterceptors);
         if (list != null)
         {
            /* Add a noop interceptor since the 3.2.3- interceptors always had
            to delegate to the next in order to dispatch the operation. Now
            there is no interceptor for this so this prevents NPEs.
            */
            list.add(new NullInterceptor());
            ctx.setInterceptors(list);
         }
      }
   }

   /**
    * 
    * @param d
    * @return
    * @throws Exception
    */ 
   protected List getInterceptors(Descriptor d) throws Exception
   {
      if (d == null)
         return null;
      Descriptor[] interceptorDescriptors = (Descriptor[]) d.getFieldValue(INTERCEPTORS);
      if( interceptorDescriptors == null )
         return null;

      ArrayList interceptors = new ArrayList();
      ClassLoader loader = Thread.currentThread().getContextClassLoader();
      for(int i = 0; i < interceptorDescriptors.length; i ++)
      {
         Descriptor desc = interceptorDescriptors[i];
         String code = (String) desc.getFieldValue("code");
         // Ignore the legacy required interceptors
         if( code.equals(ModelMBeanInterceptor.class.getName()) ||
            code.equals(ObjectReferenceInterceptor.class.getName()) ||
            code.equals(PersistenceInterceptor2.class.getName()) )
         {
            log.debug("Ignoring obsolete legacy interceptor: "+code);
            continue;
         }

         Class interceptorClass = loader.loadClass(code);
         Interceptor interceptor = null;
         // Check for a ctor(MBeanInvoker)
         Class[] ctorSig = {MBeanInvoker.class};
         try
         {
            Constructor ctor = interceptorClass.getConstructor(ctorSig);
            Object[] ctorArgs = {this};
            interceptor = (Interceptor) ctor.newInstance(ctorArgs);
         }
         catch(Throwable t)
         {
            log.debug("Failed to invoke ctor(MBeanInvoker) for: "
                  +interceptorClass, t);
            // Try the default ctor
            interceptor = (Interceptor) interceptorClass.newInstance();
         }
         interceptors.add(interceptor);

         // Apply any interceptor attributes
         String[] names = desc.getFieldNames();
         HashMap propertyMap = new HashMap();
         if( names.length > 1 )
         {
            BeanInfo beanInfo = Introspector.getBeanInfo(interceptorClass);
            PropertyDescriptor[] props = beanInfo.getPropertyDescriptors();
            for(int p = 0; p < props.length; p ++)
            {
               /* Store the property name as lower case since this is how
               descriptor field names are stored. Java5 maintains the case of
               field names so add both the original and lower case version.
               */
               String fieldName = props[p].getName();
               propertyMap.put(fieldName, props[p]);
               fieldName = fieldName.toLowerCase();
               propertyMap.put(fieldName, props[p]);
            }
            // Map each attribute to the corresponding interceptor property
            for(int n = 0; n < names.length; n ++)
            {
               String name = names[n];
               if( name.equals("code") )
                  continue;
               String text = (String) desc.getFieldValue(name);
               PropertyDescriptor pd = (PropertyDescriptor) propertyMap.get(name);
               if( pd == null )
                  throw new IntrospectionException("No PropertyDescriptor for attribute:"+name);
               Method setter = pd.getWriteMethod();
               if( setter != null )
               {
                  Class ptype = pd.getPropertyType();
                  PropertyEditor editor = PropertyEditorManager.findEditor(ptype);
                  if( editor == null )
                     throw new IntrospectionException("Cannot convert string to interceptor attribute:"+name);
                  editor.setAsText(text);
                  Object args[] = {editor.getValue()};
                  setter.invoke(interceptor, args);
               }
            }
         }
      }

      if( interceptors.size() == 0 )
         interceptors = null;
      return interceptors;
   }

   protected void setValuesFromMBeanInfo() throws JMException
   {
      for (Iterator it = attributeContextMap.entrySet().iterator(); it.hasNext(); )
      {
         Map.Entry entry = (Map.Entry)it.next();
         String key = (String)entry.getKey();

         InvocationContext ctx = (InvocationContext)entry.getValue();
         //Initialize value from descriptor.
         Object value = ctx.getDescriptor().getFieldValue(XMBeanConstants.VALUE);
         if (value != null)
         {
            setAttribute(new Attribute(key, value));
         } // end of if ()
      }

   }

   protected boolean isSupportedResourceType(Object resource, String resourceType)
   {
      if (resourceType.equals(OBJECT_REF))
         return true;

      return false;
   }

}
