/*
 * Copyright (C) MX4J. All rights reserved.
 *
 * This software is distributed under the terms of the MX4J License version 1.0. See the terms of
 * the MX4J License in the documentation provided with this software.
 */

package org.apache.geode.admin.jmx.internal;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Date;

import javax.management.Attribute;
import javax.management.AttributeChangeNotification;
import javax.management.AttributeChangeNotificationFilter;
import javax.management.AttributeList;
import javax.management.AttributeNotFoundException;
import javax.management.Descriptor;
import javax.management.InstanceNotFoundException;
import javax.management.InvalidAttributeValueException;
import javax.management.ListenerNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanRegistration;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationEmitter;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import javax.management.RuntimeErrorException;
import javax.management.RuntimeOperationsException;
import javax.management.ServiceNotFoundException;
import javax.management.loading.ClassLoaderRepository;
import javax.management.modelmbean.InvalidTargetObjectTypeException;
import javax.management.modelmbean.ModelMBean;
import javax.management.modelmbean.ModelMBeanAttributeInfo;
import javax.management.modelmbean.ModelMBeanInfo;
import javax.management.modelmbean.ModelMBeanOperationInfo;

import mx4j.ImplementationException;
import mx4j.log.FileLogger;
import mx4j.log.Log;
import mx4j.log.Logger;
import mx4j.log.MBeanLogger;
import mx4j.persist.FilePersister;
import mx4j.persist.MBeanPersister;
import mx4j.persist.PersisterMBean;
import mx4j.util.Utils;


/**
 * @author <a href="mailto:biorn_steedom@users.sourceforge.net">Simone Bordet</a>
 * @version $Revision: 1.14 $
 */
@Deprecated
public class MX4JModelMBean implements ModelMBean, MBeanRegistration, NotificationEmitter {
  private static final String OBJECT_RESOURCE_TYPE = "ObjectReference";

  private static final int ALWAYS_STALE = 1;
  private static final int NEVER_STALE = 2;
  private static final int STALE = 3;
  private static final int NOT_STALE = 4;

  private static final int PERSIST_NEVER = -1;
  private static final int PERSIST_ON_TIMER = -2;
  private static final int PERSIST_ON_UPDATE = -3;
  private static final int PERSIST_NO_MORE_OFTEN_THAN = -4;

  private MBeanServer m_mbeanServer;
  private Object m_managedResource;
  private boolean m_canBeRegistered;
  private ModelMBeanInfo m_modelMBeanInfo;
  private NotificationBroadcasterSupport m_attributeChangeBroadcaster =
      new NotificationBroadcasterSupport();
  private NotificationBroadcasterSupport m_generalBroadcaster =
      new NotificationBroadcasterSupport();

  public MX4JModelMBean() throws MBeanException, RuntimeOperationsException {
    try {
      load();
    } catch (Exception x) {
      Logger logger = getLogger();
      logger.warn("Cannot restore previously saved status", x);
    }
  }

  public MX4JModelMBean(ModelMBeanInfo info) throws MBeanException, RuntimeOperationsException {
    if (info == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "ModelMBeanInfo parameter cannot be null."));
    } else {
      setModelMBeanInfo(info);
    }
  }

  private Logger getLogger() {
    return Log.getLogger(getClass().getName());
  }

  @Override
  public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception {
    if (m_canBeRegistered) {
      m_mbeanServer = server;
      return name;
    } else {
      throw new MBeanRegistrationException(new IllegalStateException(
          "ModelMBean cannot be registered until setModelMBeanInfo has been called."));
    }
  }

  @Override
  public void postRegister(Boolean registrationDone) {
    if (!registrationDone) {
      clear();
    }
  }

  @Override
  public void preDeregister() throws Exception {}

  @Override
  public void postDeregister() {
    clear();
  }

  private void clear() {
    m_mbeanServer = null;
    m_managedResource = null;
    m_modelMBeanInfo = null;
    m_generalBroadcaster = null;
    m_attributeChangeBroadcaster = null;
    // PENDING: also remove generic listeners, attribute change listeners, log4j appenders...
  }

  @Override
  public void setModelMBeanInfo(ModelMBeanInfo modelMBeanInfo)
      throws MBeanException, RuntimeOperationsException {
    if (modelMBeanInfo == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "ModelMBeanInfo cannot be null."));
    }
    if (!isModelMBeanInfoValid(modelMBeanInfo)) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "ModelMBeanInfo is invalid."));
    }

    m_modelMBeanInfo = (ModelMBeanInfo) modelMBeanInfo.clone();

    Logger logger = getLogger();
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("ModelMBeanInfo successfully set to: " + m_modelMBeanInfo);
    }
    // Only now the MBean can be registered in the MBeanServer
    m_canBeRegistered = true;
  }

  private boolean isModelMBeanInfoValid(ModelMBeanInfo info) {
    return info != null && info.getClassName() != null;
    // PENDING: maybe more checks are needed
  }

  @Override
  public void setManagedResource(Object resource, String resourceType) throws MBeanException,
      RuntimeOperationsException, InstanceNotFoundException, InvalidTargetObjectTypeException {
    if (resource == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Managed resource cannot be null."));
    }
    if (!isResourceTypeSupported(resourceType)) {
      throw new InvalidTargetObjectTypeException(resourceType);
    }

    Logger logger = getLogger();
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Setting managed resource to be: " + resource);
    }
    m_managedResource = resource;
  }

  private boolean isResourceTypeSupported(String resourceType) {
    // For now only object reference is supported
    return OBJECT_RESOURCE_TYPE.equals(resourceType);
  }

  private Object getManagedResource() {
    return m_managedResource;
  }

  @Override
  public MBeanInfo getMBeanInfo() {
    return m_modelMBeanInfo == null ? null : (MBeanInfo) m_modelMBeanInfo.clone();
  }

  @Override
  public void addAttributeChangeNotificationListener(NotificationListener listener,
      String attributeName, Object handback)
      throws MBeanException, RuntimeOperationsException, IllegalArgumentException {
    if (listener == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Listener cannot be null."));
    }
    AttributeChangeNotificationFilter filter = new AttributeChangeNotificationFilter();
    if (attributeName != null) {
      filter.enableAttribute(attributeName);
    } else {
      MBeanAttributeInfo[] ai = m_modelMBeanInfo.getAttributes();
      for (final MBeanAttributeInfo mBeanAttributeInfo : ai) {
        Descriptor d = mBeanAttributeInfo.getDescriptor();
        filter.enableAttribute((String) d.getFieldValue("name"));
      }
    }

    getAttributeChangeBroadcaster().addNotificationListener(listener, filter, handback);

    Logger logger = getLogger();
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Listener " + listener + " for attribute " + attributeName
          + " added successfully, handback is " + handback);
    }
  }

  @Override
  public void addNotificationListener(NotificationListener listener, NotificationFilter filter,
      Object handback) throws IllegalArgumentException {
    m_generalBroadcaster.addNotificationListener(listener, filter, handback);
  }

  @Override
  public MBeanNotificationInfo[] getNotificationInfo() {
    return m_modelMBeanInfo.getNotifications();
  }

  @Override
  public void removeAttributeChangeNotificationListener(NotificationListener listener,
      String attributeName) throws RuntimeOperationsException, ListenerNotFoundException {
    try {
      removeAttributeChangeNotificationListener(listener, attributeName, null);
    } catch (MBeanException e) {
      throw new RuntimeOperationsException(new RuntimeException(e.getMessage()));
    }
  }

  // Not in the spec but needed
  private void removeAttributeChangeNotificationListener(NotificationListener listener,
      String attributeName, Object handback)
      throws MBeanException, RuntimeOperationsException, ListenerNotFoundException {
    if (listener == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Listener cannot be null."));
    }
    AttributeChangeNotificationFilter filter = new AttributeChangeNotificationFilter();
    if (attributeName != null) {
      filter.enableAttribute(attributeName);
    } else {
      MBeanAttributeInfo[] ai = m_modelMBeanInfo.getAttributes();
      for (final MBeanAttributeInfo mBeanAttributeInfo : ai) {
        Descriptor d = mBeanAttributeInfo.getDescriptor();
        filter.enableAttribute((String) d.getFieldValue("name"));
      }
    }

    getAttributeChangeBroadcaster().removeNotificationListener(listener, filter, handback);

    Logger logger = getLogger();
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Listener " + listener + " for attribute " + attributeName
          + " removed successfully, handback is " + handback);
    }
  }

  @Override
  public void removeNotificationListener(NotificationListener listener)
      throws RuntimeOperationsException, ListenerNotFoundException {
    m_generalBroadcaster.removeNotificationListener(listener);
  }

  @Override
  public void removeNotificationListener(NotificationListener listener, NotificationFilter filter,
      Object handback) throws RuntimeOperationsException, ListenerNotFoundException {
    m_generalBroadcaster.removeNotificationListener(listener, filter, handback);
  }

  @Override
  public void sendAttributeChangeNotification(Attribute oldAttribute, Attribute newAttribute)
      throws MBeanException, RuntimeOperationsException {
    if (oldAttribute == null || newAttribute == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Attribute cannot be null."));
    }
    if (!oldAttribute.getName().equals(newAttribute.getName())) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Attribute names cannot be different."));
    }

    // TODO: the source must be the object name of the MBean if the listener was registered through
    // MBeanServer
    Object oldValue = oldAttribute.getValue();
    AttributeChangeNotification n = new AttributeChangeNotification(this, 1,
        System.currentTimeMillis(), "Attribute value changed", oldAttribute.getName(),
        oldValue == null ? null : oldValue.getClass().getName(), oldValue, newAttribute.getValue());
    sendAttributeChangeNotification(n);
  }

  @Override
  public void sendAttributeChangeNotification(AttributeChangeNotification notification)
      throws MBeanException, RuntimeOperationsException {
    if (notification == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Notification cannot be null."));
    }

    getAttributeChangeBroadcaster().sendNotification(notification);

    Logger modelMBeanLogger = getModelMBeanLogger(notification.getType());
    if (modelMBeanLogger != null) {
      if (modelMBeanLogger.isEnabledFor(Logger.DEBUG)) {
        modelMBeanLogger.debug("ModelMBean log: " + new Date() + " - " + notification);
      }
    }

    Logger logger = getLogger();
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Attribute change notification " + notification + " sent");
    }
  }

  @Override
  public void sendNotification(String message) throws MBeanException, RuntimeOperationsException {
    Notification notification = new Notification("jmx.modelmbean.general", this, 1, message);
    sendNotification(notification);
  }

  @Override
  public void sendNotification(Notification notification)
      throws MBeanException, RuntimeOperationsException {
    if (m_generalBroadcaster != null) {
      m_generalBroadcaster.sendNotification(notification);
    }
  }

  @Override
  public AttributeList getAttributes(String[] attributes) {
    if (attributes == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Attribute names cannot be null."));
    }

    Logger logger = getLogger();

    AttributeList list = new AttributeList();
    for (String attrName : attributes) {
      Attribute attribute = null;
      try {
        Object value = getAttribute(attrName);
        attribute = new Attribute(attrName, value);
        list.add(attribute);
      } catch (Exception x) {
        if (logger.isEnabledFor(Logger.TRACE)) {
          logger.trace("getAttribute for attribute " + attrName + " failed", x);
        }
        // And go on with the next attribute
      }
    }
    return list;
  }

  @Override
  public Object getAttribute(String attribute)
      throws AttributeNotFoundException, MBeanException, ReflectionException {
    if (attribute == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Attribute name cannot be null."));
    }

    Logger logger = getLogger();

    // I want the real info, not its clone
    ModelMBeanInfo info = getModelMBeanInfo();
    if (info == null) {
      throw new AttributeNotFoundException(
          "ModelMBeanInfo is null");
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("ModelMBeanInfo is: " + info);
    }

    // This is a clone, we use it read only
    ModelMBeanAttributeInfo attrInfo = info.getAttribute(attribute);
    if (attrInfo == null) {
      throw new AttributeNotFoundException(
          String.format("Cannot find ModelMBeanAttributeInfo for attribute %s",
              attribute));
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Attribute info is: " + attrInfo);
    }
    if (!attrInfo.isReadable()) {
      throw new AttributeNotFoundException(
          String.format("Attribute %s is not readable", attribute));
    }

    // This returns a clone of the mbean descriptor, we use it read only
    Descriptor mbeanDescriptor = info.getMBeanDescriptor();
    if (mbeanDescriptor == null) {
      throw new AttributeNotFoundException(
          "MBean descriptor cannot be null");
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("MBean descriptor is: " + mbeanDescriptor);
    }

    // This descriptor is a clone
    Descriptor attributeDescriptor = attrInfo.getDescriptor();
    if (attributeDescriptor == null) {
      throw new AttributeNotFoundException(
          String.format("Attribute descriptor for attribute %s cannot be null",
              attribute));
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Attribute descriptor is: " + attributeDescriptor);
    }

    Object returnValue = null;

    String lastUpdateField = "lastUpdatedTimeStamp";

    int staleness = getStaleness(attributeDescriptor, mbeanDescriptor, lastUpdateField);

    if (staleness == ALWAYS_STALE || staleness == STALE) {
      if (logger.isEnabledFor(Logger.TRACE)) {
        logger.trace("Value is stale");
      }

      String getter = (String) attributeDescriptor.getFieldValue("getMethod");
      if (logger.isEnabledFor(Logger.DEBUG)) {
        logger.debug("getMethod field is: " + getter);
      }
      if (getter == null) {
        // No getter, use default value
        returnValue = attributeDescriptor.getFieldValue("default");

        if (returnValue != null) {
          // Check if the return type is of the same type
          // As an extension allow covariant return type
          Class returned = returnValue.getClass();
          Class declared = loadClassWithContextClassLoader(attrInfo.getType());

          checkAssignability(returned, declared);
        }

        if (logger.isEnabledFor(Logger.DEBUG)) {
          logger.debug(
              "getAttribute for attribute " + attribute + " returns default value: " + returnValue);
        }
      } else {
        if (logger.isEnabledFor(Logger.TRACE)) {
          logger.trace("Invoking attribute getter...");
        }
        // As an extension, allow attributes to be called on target objects also
        Object target = resolveTargetObject(attributeDescriptor);
        returnValue = invokeMethod(target, getter, new Class[0], new Object[0]);
        if (logger.isEnabledFor(Logger.DEBUG)) {
          logger.debug("Returned value is: " + returnValue);
        }

        if (returnValue != null) {
          // Check if the return type is of the same type
          // As an extension allow covariant return type
          Class returned = returnValue.getClass();
          Class declared = loadClassWithContextClassLoader(attrInfo.getType());

          checkAssignability(returned, declared);
        }

        // Cache the new value only if caching is needed
        if (staleness != ALWAYS_STALE) {
          attributeDescriptor.setField("value", returnValue);
          attributeDescriptor.setField(lastUpdateField, System.currentTimeMillis());
          if (logger.isEnabledFor(Logger.TRACE)) {
            logger.trace("Returned value has been cached");
          }

          // And now replace the descriptor with the updated clone
          info.setDescriptor(attributeDescriptor, "attribute");
        }

        if (logger.isEnabledFor(Logger.DEBUG)) {
          logger.debug(
              "getAttribute for attribute " + attribute + " returns invoked value: " + returnValue);
        }
      }
    } else {
      // Return cached value
      returnValue = attributeDescriptor.getFieldValue("value");

      if (returnValue != null) {
        // Check if the return type is of the same type
        // As an extension allow covariant return type
        Class returned = returnValue.getClass();
        Class declared = loadClassWithContextClassLoader(attrInfo.getType());

        checkAssignability(returned, declared);
      }

      if (logger.isEnabledFor(Logger.DEBUG)) {
        logger.debug(
            "getAttribute for attribute " + attribute + " returns cached value: " + returnValue);
      }
    }

    // Puff, everything went ok
    return returnValue;
  }

  @Override
  public AttributeList setAttributes(AttributeList attributes) {
    if (attributes == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Attribute list cannot be null."));
    }

    Logger logger = getLogger();

    AttributeList list = new AttributeList();
    for (final Object o : attributes) {
      Attribute attribute = (Attribute) o;
      String name = attribute.getName();
      try {
        setAttribute(attribute);
        list.add(attribute);
      } catch (Exception x) {
        if (logger.isEnabledFor(Logger.TRACE)) {
          logger.trace("setAttribute for attribute " + name + " failed", x);
        }
        // And go on with the next one
      }
    }
    return list;
  }

  @Override
  public void setAttribute(Attribute attribute) throws AttributeNotFoundException,
      InvalidAttributeValueException, MBeanException, ReflectionException {
    if (attribute == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Attribute cannot be null."));
    }

    Logger logger = getLogger();

    // No need to synchronize: I work mostly on clones
    // I want the real info, not its clone
    ModelMBeanInfo info = getModelMBeanInfo();
    if (info == null) {
      throw new AttributeNotFoundException(
          "ModelMBeanInfo is null");
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("ModelMBeanInfo is: " + info);
    }

    String attrName = attribute.getName();
    Object attrValue = attribute.getValue();

    // This is a clone, we use it read only
    ModelMBeanAttributeInfo attrInfo = info.getAttribute(attrName);
    if (attrInfo == null) {
      throw new AttributeNotFoundException(
          String.format("Cannot find ModelMBeanAttributeInfo for attribute %s",
              attrName));
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Attribute info is: " + attrInfo);
    }

    if (!attrInfo.isWritable()) {
      throw new AttributeNotFoundException(
          String.format("Attribute %s is not writable", attrName));
    }

    // This returns a clone of the mbean descriptor, we use it read only
    Descriptor mbeanDescriptor = info.getMBeanDescriptor();
    if (mbeanDescriptor == null) {
      throw new AttributeNotFoundException(
          "MBean descriptor cannot be null");
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("MBean descriptor is: " + mbeanDescriptor);
    }

    // This descriptor is a clone
    Descriptor attributeDescriptor = attrInfo.getDescriptor();
    if (attributeDescriptor == null) {
      throw new AttributeNotFoundException(
          String.format("Attribute descriptor for attribute %s cannot be null",
              attrName));
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Attribute descriptor is: " + attributeDescriptor);
    }

    String lastUpdateField = "lastUpdatedTimeStamp";

    Object oldValue = null;
    try {
      oldValue = getAttribute(attrName);
      if (logger.isEnabledFor(Logger.DEBUG)) {
        logger.debug("Previous value of attribute " + attrName + ": " + oldValue);
      }
    } catch (Exception x) {
      if (logger.isEnabledFor(Logger.DEBUG)) {
        logger.debug("Cannot get previous value of attribute " + attrName, x);
      }
    }

    // Check if setMethod is present
    String method = (String) attributeDescriptor.getFieldValue("setMethod");
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("setMethod field is: " + method);
    }
    if (method != null) {
      Class declared = loadClassWithContextClassLoader(attrInfo.getType());
      if (attrValue != null) {
        Class parameter = attrValue.getClass();
        checkAssignability(parameter, declared);
      }

      // As an extension, allow attributes to be called on target objects also
      Object target = resolveTargetObject(attributeDescriptor);
      invokeMethod(target, method, new Class[] {declared}, new Object[] {attrValue});

      // Cache the value only if currencyTimeLimit is not 0, ie it is not always stale
      int staleness = getStaleness(attributeDescriptor, mbeanDescriptor, lastUpdateField);
      if (staleness != ALWAYS_STALE) {
        attributeDescriptor.setField("value", attrValue);
        attributeDescriptor.setField(lastUpdateField, System.currentTimeMillis());
        if (logger.isEnabledFor(Logger.TRACE)) {
          logger.trace("Attribute's value has been cached");
        }
      } else {
        if (logger.isEnabledFor(Logger.TRACE)) {
          logger.trace("Always stale, avoiding to cache attribute's value");
        }
      }
    } else {
      if (attrValue != null) {
        Class parameter = attrValue.getClass();
        Class declared = loadClassWithContextClassLoader(attrInfo.getType());

        checkAssignability(parameter, declared);
      }

      // Always store the value in the descriptor: no setMethod
      attributeDescriptor.setField("value", attrValue);
    }

    // And now replace the descriptor with the updated clone
    info.setDescriptor(attributeDescriptor, "attribute");

    // Send notifications to listeners
    if (logger.isEnabledFor(Logger.TRACE)) {
      logger.trace("Sending attribute change notifications");
    }
    sendAttributeChangeNotification(new Attribute(attrName, oldValue), attribute);

    // Persist this ModelMBean
    boolean persistNow = shouldPersistNow(attributeDescriptor, mbeanDescriptor, lastUpdateField);
    if (persistNow) {
      if (logger.isEnabledFor(Logger.TRACE)) {
        logger.trace("Persisting this ModelMBean...");
      }
      try {
        store();
        if (logger.isEnabledFor(Logger.TRACE)) {
          logger.trace("ModelMBean persisted successfully");
        }
      } catch (Exception x) {
        logger.error("Cannot store ModelMBean after setAttribute", x);
        if (x instanceof MBeanException) {
          throw (MBeanException) x;
        } else {
          throw new MBeanException(x);
        }
      }
    }
  }

  @Override
  public Object invoke(String method, Object[] arguments, String[] params)
      throws MBeanException, ReflectionException {
    if (method == null) {
      throw new RuntimeOperationsException(new IllegalArgumentException(
          "Method name cannot be null."));
    }
    if (arguments == null) {
      arguments = new Object[0];
    }
    if (params == null) {
      params = new String[0];
    }

    Logger logger = getLogger();

    // Find operation descriptor
    ModelMBeanInfo info = getModelMBeanInfo();
    if (info == null) {
      throw new MBeanException(new ServiceNotFoundException(
          "ModelMBeanInfo is null"));
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("ModelMBeanInfo is: " + info);
    }

    // This is a clone, we use it read only
    ModelMBeanOperationInfo operInfo = info.getOperation(method);
    if (operInfo == null) {
      throw new MBeanException(new ServiceNotFoundException(
          String.format("Cannot find ModelMBeanOperationInfo for operation %s",
              method)));
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Operation info is: " + operInfo);
    }

    // This descriptor is a clone
    Descriptor operationDescriptor = operInfo.getDescriptor();
    if (operationDescriptor == null) {
      throw new MBeanException(new ServiceNotFoundException(
          String.format("Operation descriptor for operation %s cannot be null",
              method)));
    }
    String role = (String) operationDescriptor.getFieldValue("role");
    if (role == null || !role.equals("operation")) {
      throw new MBeanException(new ServiceNotFoundException(
          String.format("Operation descriptor field 'role' must be 'operation', not %s",
              role)));
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("Operation descriptor is: " + operationDescriptor);
    }

    // This returns a clone of the mbean descriptor, we use it read only
    Descriptor mbeanDescriptor = info.getMBeanDescriptor();
    if (mbeanDescriptor == null) {
      throw new MBeanException(new ServiceNotFoundException(
          "MBean descriptor cannot be null"));
    }
    if (logger.isEnabledFor(Logger.DEBUG)) {
      logger.debug("MBean descriptor is: " + mbeanDescriptor);
    }

    Object returnValue = null;

    String lastUpdateField = "lastReturnedTimeStamp";

    // Check if the method should be invoked given the cache settings
    int staleness = getStaleness(operationDescriptor, mbeanDescriptor, lastUpdateField);

    if (staleness == ALWAYS_STALE || staleness == STALE) {
      if (logger.isEnabledFor(Logger.TRACE)) {
        logger.trace("Value is stale");
      }

      // Find parameters classes
      Class[] parameters = null;
      try {
        parameters = Utils.loadClasses(Thread.currentThread().getContextClassLoader(), params);
      } catch (ClassNotFoundException x) {
        logger.error("Cannot find operation's parameter classes", x);
        throw new ReflectionException(x);
      }

      if (logger.isEnabledFor(Logger.TRACE)) {
        logger.trace("Invoking operation...");
      }

      // Find target object
      Object target = resolveTargetObject(operationDescriptor);
      returnValue = invokeMethod(target, method, parameters, arguments);

      if (logger.isEnabledFor(Logger.DEBUG)) {
        logger.debug("Returned value is: " + returnValue);
      }

      if (returnValue != null) {
        Class parameter = returnValue.getClass();
        Class declared = loadClassWithContextClassLoader(operInfo.getReturnType());

        checkAssignability(parameter, declared);
      }

      // Cache the new value only if caching is needed
      if (staleness != ALWAYS_STALE) {
        operationDescriptor.setField("lastReturnedValue", returnValue);
        operationDescriptor.setField(lastUpdateField, System.currentTimeMillis());
        if (logger.isEnabledFor(Logger.TRACE)) {
          logger.trace("Returned value has been cached");
        }

        // And now replace the descriptor with the updated clone
        info.setDescriptor(operationDescriptor, "operation");
      }

      if (logger.isEnabledFor(Logger.DEBUG)) {
        logger.debug("invoke for operation " + method + " returns invoked value: " + returnValue);
      }
    } else {
      // Return cached value
      returnValue = operationDescriptor.getFieldValue("lastReturnedValue");

      if (returnValue != null) {
        Class parameter = returnValue.getClass();
        Class declared = loadClassWithContextClassLoader(operInfo.getReturnType());

        checkAssignability(parameter, declared);
      }

      if (logger.isEnabledFor(Logger.DEBUG)) {
        logger.debug("invoke for operation " + method + " returns cached value: " + returnValue);
      }
    }

    // As an extension, persist this model mbean also after operation invocation, but using only
    // settings provided in the operation descriptor, without falling back to defaults set in
    // the MBean descriptor
    boolean persistNow = shouldPersistNow(operationDescriptor, null, lastUpdateField);
    int impact = operInfo.getImpact();
    if (persistNow && impact != MBeanOperationInfo.INFO) {
      if (logger.isEnabledFor(Logger.TRACE)) {
        logger.trace("Persisting this ModelMBean...");
      }
      try {
        store();
        if (logger.isEnabledFor(Logger.TRACE)) {
          logger.trace("ModelMBean persisted successfully");
        }
      } catch (Exception x) {
        logger.error(
            "Cannot store ModelMBean after operation invocation", x);
        if (x instanceof MBeanException) {
          throw (MBeanException) x;
        } else {
          throw new MBeanException(x);
        }
      }
    }

    return returnValue;
  }

  private Object resolveTargetObject(Descriptor descriptor) throws MBeanException {
    Logger logger = getLogger();
    Object target = descriptor.getFieldValue("targetObject");
    if (logger.isEnabledFor(Logger.TRACE)) {
      logger.trace("targetObject is: " + target);
    }
    if (target == null) {
      target = getManagedResource();
    } else {
      String targetObjectType = (String) descriptor.getFieldValue("targetObjectType");
      if (logger.isEnabledFor(Logger.TRACE)) {
        logger.trace("targetObjectType is: " + targetObjectType);
      }
      if (targetObjectType == null) {
        // Not defined, assume object reference
        targetObjectType = OBJECT_RESOURCE_TYPE;
      }

      if (!isResourceTypeSupported(targetObjectType)) {
        throw new MBeanException(new InvalidTargetObjectTypeException(targetObjectType));
      }
    }
    return target;
  }

  @Override
  public void load() throws MBeanException, RuntimeOperationsException, InstanceNotFoundException {
    PersisterMBean persister = findPersister();
    if (persister != null) {
      ModelMBeanInfo info = (ModelMBeanInfo) persister.load();
      setModelMBeanInfo(info);
    }
  }

  @Override
  public void store() throws MBeanException, RuntimeOperationsException, InstanceNotFoundException {
    PersisterMBean persister = findPersister();
    if (persister != null) {
      // Take a clone to avoid synchronization problems
      ModelMBeanInfo info = (ModelMBeanInfo) getMBeanInfo();
      persister.store(info);
    }
  }

  protected ClassLoaderRepository getClassLoaderRepository() {
    if (m_mbeanServer != null) {
      return m_mbeanServer.getClassLoaderRepository();
    } else {
      return null;
    }
  }


  private boolean shouldPersistNow(Descriptor attribute, Descriptor mbean, String lastUpdateField) {
    int persist = getPersistPolicy(attribute, mbean);
    if (persist == PERSIST_NO_MORE_OFTEN_THAN) {
