/*
 * Copyright (c) 1998-2012 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Scott Ferguson
 */

package com.caucho.jmx;

import com.caucho.loader.*;
import com.caucho.util.L10N;

import javax.management.*;
import javax.management.loading.ClassLoaderRepository;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.logging.*;
import java.io.Closeable;

/**
 * The context containing mbeans registered at a particular level.
 */
public class MBeanContext
{
  private static final Logger log
    = Logger.getLogger(MBeanContext.class.getName());
  private static final L10N L = new L10N(MBeanContext.class);

  /*
  private final EnvironmentLocal<MBeanClose> _mbeanClose
    = new EnvironmentLocal<MBeanClose>();
  */

  private MBeanContext _parent;
  
  // The owning MBeanServer
  private AbstractMBeanServer _mbeanServer;

  private MBeanContext _globalContext;

  private MBeanServerDelegate _delegate;
  private long _seq;
  
  // class loader for this server
  private ClassLoader _loader;

  private String _domain = "resin";
  private LinkedHashMap<String,String> _properties
    = new LinkedHashMap<String,String>();

  private ClassLoaderRepositoryImpl _classLoaderRepository
    = new ClassLoaderRepositoryImpl();
  
  // map of all mbeans
  private Hashtable<ObjectName,MBeanWrapper> _mbeans
    = new Hashtable<ObjectName,MBeanWrapper>();

  private ArrayList<Listener> _listeners = new ArrayList<Listener>();

  // The local view associated with the context
  private MBeanView _view;
  // The global view associated with the context
  private MBeanView _globalView;

  MBeanContext(AbstractMBeanServer mbeanServer,
               ClassLoader loader,
               MBeanServerDelegate delegate,
               MBeanContext globalContext)
  {
    loader = Environment.getEnvironmentClassLoader(loader);

    if (loader == null)
      loader = ClassLoader.getSystemClassLoader();
    
    _mbeanServer = mbeanServer;
    _loader = loader;
    _delegate = delegate;
    _globalContext = globalContext;

    _mbeanServer.setCurrentContext(this, loader);

    Environment.addClassLoaderListener(new CloseListener(this), _loader);
    //Environment.addClassLoaderListener(new WeakCloseListener(this), _loader);
    
    _classLoaderRepository.addClassLoader(_loader);

    _view = new MBeanView(mbeanServer, _loader, "resin");
    _globalView = new MBeanView(mbeanServer, _loader, "resin");

    if (_loader != null
        && _loader != ClassLoader.getSystemClassLoader()) {
      _parent = _mbeanServer.createContext(_loader.getParent());

      if (_parent == this) {
        _parent = null;
      }
    }
  }

  /**
   * Returns the parent view.
   */
  protected MBeanView getParentView()
  {
    return _mbeanServer.getParentView();
  }

  /**
   * Returns the ClassLoaderRepository.
   */
  public ClassLoaderRepository getClassLoaderRepository()
  {
    return _classLoaderRepository;
  }

  /**
   * Returns the class loader.
   */
  public ClassLoader getClassLoader()
  {
    return _loader;
  }

  /**
   * Returns the view for this context.
   */
  MBeanView getView()
  {
    return _view;
  }

  /**
   * Returns the view for this context.
   */
  MBeanView getGlobalView()
  {
    return _globalView;
  }

  /**
   * Sets the properties.
   */
  public void setProperties(Map<String,String> props)
  {
    _properties.clear();
    _properties.putAll(props);
  }

  /**
   * Sets the properties.
   */
  public LinkedHashMap<String,String> copyProperties()
  {
    return new LinkedHashMap<String,String>(_properties);
  }

  /**
   * Returns the object name.
   */
  public ObjectName getObjectName(String name)
    throws MalformedObjectNameException
  {
    int len = name.length();

    for (int i = 0; i < len; i++) {
      char ch = name.charAt(i);

      if (ch == ':')
        return new ObjectName(name);
      else if ('a' <= ch && ch <= 'z'
               || 'A' <= ch && ch <= 'Z'
               || '0' <= ch && ch <= '9'
               || ch == '-' || ch == '_' || ch == '.') {
        continue;
      }
      else
        break;
    }
    
    LinkedHashMap<String,String> properties;
    properties = new LinkedHashMap<String,String>();

    properties.putAll(_properties);
    Jmx.parseProperties(properties, name);

    return Jmx.getObjectName(_domain, properties);
  }

  /**
   * Finds an admin object.
   */
  MBeanWrapper getMBean(ObjectName name)
  {
    if (name != null)
      return _mbeans.get(name);
    else
      return null;
  }
  
  /**
   * Registers an MBean with the server.
   *
   * @param mbean the object to be registered as an MBean
   * @param name the name of the mbean.
   *
   * @return the instantiated object.
   */
  ObjectInstance registerMBean(MBeanWrapper mbean,
                               ObjectName name)
    throws InstanceAlreadyExistsException,
           MBeanRegistrationException,
           NotCompliantMBeanException
  {
    if (mbean == null)
      throw new NotCompliantMBeanException(L.l("{0} is a null mbean",
                                               name));
    if (_mbeans.get(name) != null)
      throw new InstanceAlreadyExistsException(String.valueOf(name));

    Object object = mbean.getObject();
    
    MBeanRegistration registration = null;
   
    if (object instanceof MBeanRegistration)
      registration = (MBeanRegistration) object;
    
    try {
      if (registration != null)
        name = registration.preRegister(_mbeanServer, name);
    } catch (Exception e) {
      throw new MBeanRegistrationException(e);
    }

    if (log.isLoggable(Level.FINEST)
        && ! name.equals(AbstractMBeanServer.SERVER_DELEGATE_NAME)) {
      log.finest(getDebugName(name, mbean) + " registered in " + this);
    }

    addMBean(name, mbean);

    /*
    // server/21c4
    if (_loader != Thread.currentThread().getContextClassLoader()) {
      MBeanClose close = _mbeanClose.getLevel();
      if (close == null) {
        close = new MBeanClose();
        _mbeanClose.set(close);
        Environment.addCloseListener(close);
      }
      close.addName(name);
    }
    */

    try {
      if (registration != null)
        registration.postRegister(new Boolean(true));
    } catch (Exception e) {
      throw new MBeanRegistrationException(e);
    }

    if (_globalContext != null)
      _globalContext.addMBean(name, mbean);

    return mbean.getObjectInstance();
  }
  
  /**
   * Unregisters an MBean from the server.
   *
   * @param name the name of the mbean.
   */
  public void unregisterMBean(ObjectName name)
    throws InstanceNotFoundException,
           MBeanRegistrationException
  {
    if (_mbeans == null) {
      removeMBean(name);
      return;
    }

    if (name.getDomain().equals("JMImplementation"))
      return;
    
    MBeanWrapper mbean = _mbeans.get(name);

    if (mbean == null)
      throw new InstanceNotFoundException(String.valueOf(name));

    Object obj = mbean.getObject();
    MBeanRegistration registration = null;
   
    if (obj instanceof MBeanRegistration)
      registration = (MBeanRegistration) obj;

    try {
      if (registration != null) {
        try {
          registration.preDeregister();
        } catch (Throwable e) {
          log.log(Level.WARNING, e.toString());
        }
      }

      removeMBean(name);
       
      if (registration != null) {
        try {
          registration.postDeregister();
        } catch (Throwable e) {
          log.log(Level.WARNING, e.toString());
        }
      }
    } catch (Exception e) {
      throw new MBeanRegistrationException(e);
    }
  }
  
  /**
   * Returns the MBean registered with the given name.
   *
   * @param name the name of the mbean.
   *
   * @return the matching mbean object.
   */
  public ObjectInstance getObjectInstance(ObjectName name)
    throws InstanceNotFoundException
  {
    MBeanWrapper mbean = getMBean(name);

    if (mbean == null)
      throw new InstanceNotFoundException(String.valueOf(name));

    return mbean.getObjectInstance();
  }

  /**
   * Returns true if the given object is registered with the server.
   *
   * @param name the name of the mbean to test.
   *
   * @return true if the object is registered.
   */
  public boolean isRegistered(ObjectName name)
  {
    return _mbeans.get(name) != null;
  }
  
  /**
   * Returns the number of MBeans registered.
   *
   * @return the number of registered mbeans.
   */
  public int getMBeanCount()
  {
    return _mbeans.size();
  }

  /**
   * Adds an object.
   */
  private void addMBean(ObjectName name, MBeanWrapper mbean)
  {
    if (_mbeans == null)
      throw new IllegalStateException(L.l("Adding MBean when context is closed"));
    
    if (mbean == null) {
      log.warning(L.l("'{0}' is an empty mbean", name));
      return;
    }

    // at finest to avoid double logging for context and global
    if (log.isLoggable(Level.FINEST))
      log.finest(getDebugName(name, mbean) + " registered in " + this);

    //log.fine(L.l("{0} registered in {1}", getDebugName(name, mbean), this));

    _mbeans.put(name, mbean);

    _view.add(name, mbean, true);
    _globalView.add(name, mbean, true);

    sendRegisterNotification(name);

    for (MBeanContext parentContext = _parent;
         parentContext != null;
         parentContext = parentContext._parent) {
      if (parentContext._globalView == null) {
        log.finer("global view is empty");
      }
      else if (parentContext._globalView.add(name, mbean, false))
        parentContext.sendRegisterNotification(name);
    }
  }

  /**
   * Removes an object.
   */
  private MBeanWrapper removeMBean(ObjectName name)
  {
    MBeanWrapper mbean = null;

    if (_mbeans != null)
      mbean = _mbeans.remove(name);

    if (_globalContext != null && _globalContext._mbeans != null)
      _globalContext._mbeans.remove(name);

    if (_view != null)
      _view.remove(name);

    if (_globalView != null && _globalView.remove(name) != null) {
      try {
        sendUnregisterNotification(name);
      } catch (Throwable e) {
        log.log(Level.WARNING, e.toString(), e);
      }
    }

    if (_parent != null)
      _parent.removeMBean(name);

    return mbean;
  }
  
  /**
   * Adds a listener to a registered MBean
   *
   * @param name the name of the mbean
   * @param listener the listener object
   * @param filter filters events the listener is interested in
   * @param handback context to be returned to the listener
   */
  void addNotificationListener(ObjectName name,
                               NotificationListener listener,
                               NotificationFilter filter,
                               Object handback)
  {
    synchronized (_listeners) {
      _listeners.add(new Listener(name, listener, filter, handback));
    }
  }
  
  /**
   * Removes a listener to a registered MBean
   *
   * @param mbean the name of the mbean
   * @param listener the listener object
   */
  public void removeNotificationListener(ObjectName mbean,
                                         NotificationListener listener)
  {
    synchronized (_listeners) {
      for (int i = _listeners.size() - 1; i >= 0; i--) {
        Listener oldListener = _listeners.get(i);

        if (oldListener.match(mbean, listener))
          _listeners.remove(i);
      }
    }
  }
  
  /**
   * Removes a listener to a registered MBean
   *
   * @param mbean the name of the mbean
   * @param listener the listener object
   * @param filter filters events the listener is interested in
   * @param handback context to be returned to the listener
   */
  public void removeNotificationListener(ObjectName mbean,
                                         NotificationListener listener,
                                         NotificationFilter filter,
                                         Object handback)
  {
    synchronized (_listeners) {
      for (int i = _listeners.size() - 1; i >= 0; i--) {
        Listener oldListener = _listeners.get(i);

        if (oldListener.match(mbean, listener, filter, handback))
          _listeners.remove(i);
      }
    }
  }

  /**
   * Sends the register notification.
   */
  void sendRegisterNotification(ObjectName name)
  {
    serverNotification(name,
                       MBeanServerNotification.REGISTRATION_NOTIFICATION);
  }

  /**
   * Sends the register notification.
   */
  void sendUnregisterNotification(ObjectName name)
  {
    serverNotification(name,
                       MBeanServerNotification.UNREGISTRATION_NOTIFICATION);
  }

  /**
   * Sends the notification
   */
  private void serverNotification(ObjectName name, String type)
  {
    MBeanServerNotification notif;

    ObjectName delegateName = AbstractMBeanServer.SERVER_DELEGATE_NAME;
    
    notif = new MBeanServerNotification(type, delegateName, _seq++, name);

    _delegate.sendNotification(notif);
  }

  /**
   * Closes the context server.
   */
  public void destroy()
  {
    try {
      if (_mbeans == null)
        return;
    
      log.finest(this + " destroy");
    
      ArrayList<ObjectName> list = new ArrayList<ObjectName>(_mbeans.keySet());

      ArrayList<Listener> listeners = new ArrayList<Listener>(_listeners);

      for (int i = 0; i < listeners.size(); i++) {
        Listener listener = listeners.get(i);

        try {
          MBeanWrapper mbean = _globalView.getMBean(listener.getName());

          if (mbean != null)
            mbean.removeNotificationListener(listener.getListener(),
                                             listener.getFilter(),
                                             listener.getHandback());
        } catch (Throwable e) {
          log.log(Level.FINER, e.toString(), e);
        }
      }

      for (int i = 0; i < list.size(); i++) {
        ObjectName name = list.get(i);

        try {
          unregisterMBean(name);
        } catch (Throwable e) {
          log.log(Level.FINE, e.toString(), e);
        }
      }

      _mbeanServer.removeContext(this, _loader);
    } finally {
      _parent = null;
      _mbeanServer = null;
      _globalContext = null;
      _delegate = null;
      _loader = null;
      _classLoaderRepository = null;
      _mbeans = null;
      _listeners = null;
      _view = null;
      _globalView = null;
    }
  }

  /**
   * Returns the debug name for a registered mbean.
   *
   * @param name the name of the mbean
   * @param mbean the mbean instance
   * @return
   */
  private String getDebugName(ObjectName name, MBeanWrapper mbean)
  {
    String className = mbean.getMBeanInfo().getClassName();

    int p = className.lastIndexOf('.');
    if (p > 0)
      className = className.substring(p + 1);

    return className + "[" + name + "]";
  }

  /**
   * Display name.
   */
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _loader + "]";
  }

  /**
   * Finalizer.
   */
  /*
  protected void finalize()
    throws Throwable
  {
    super.finalize();
    
    destroy();
  }
  */

  /**
   * Listener references.
   */
  static class Listener {
    private ObjectName _name;
    private WeakReference<NotificationListener> _listenerRef;
    private WeakReference<NotificationFilter> _filterRef;
    private WeakReference<Object> _handbackRef;

    Listener(ObjectName name,
             NotificationListener listener,
             NotificationFilter filter,
             Object handback)
    {
      _name = name;
      _listenerRef = new WeakReference<NotificationListener>(listener);

      if (filter != null)
        _filterRef = new WeakReference<NotificationFilter>(filter);

      if (handback != null)
        _handbackRef = new WeakReference<Object>(handback);
    }

    ObjectName getName()
    {
      return _name;
    }

    NotificationListener getListener()
    {
      return _listenerRef.get();
    }

    NotificationFilter getFilter()
    {
      return _filterRef != null ? _filterRef.get() : null;
    }

    Object getHandback()
    {
      return _handbackRef != null ? _handbackRef.get() : null;
    }

    boolean match(ObjectName name,
                  NotificationListener listener,
                  NotificationFilter filter,
                  Object handback)
    {
      if (! _name.equals(name))
        return false;

      else if (listener != _listenerRef.get())
        return false;

      else if (filter == null && _filterRef != null)
        return false;
      
      else if (_filterRef != null && _filterRef.get() != filter)
        return false;

      else if (handback == null && _handbackRef != null)
        return false;
      
      else if (_handbackRef != null && _handbackRef.get() != handback)
        return false;
      
      else
        return true;
    }

    boolean match(ObjectName name,
                  NotificationListener listener)
    {
      if (! _name.equals(name))
        return false;

      else if (listener != _listenerRef.get())
        return false;
      
      else
        return true;
    }
  }

  public class MBeanClose implements Closeable {
    private final ArrayList<ObjectName> _names = new ArrayList<ObjectName>();

    public void addName(ObjectName name)
    {
      _names.add(name);
    }

    public void removeName(ObjectName name)
    {
      _names.add(name);
    }

    public void close()
    {
      ArrayList<ObjectName> names = new ArrayList<ObjectName>(_names);
      _names.clear();

      for (ObjectName name : names) {
        try {
          unregisterMBean(name);
        } catch (Exception e) {
          log.log(Level.FINEST, e.toString(), e);
        }
      }
    }

    public String toString()
    {
      return getClass().getSimpleName();
    }
  }
}

