/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2004 Bull S.A.
 * Contact: jonas-team@objectweb.org
 *
 * This library 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 any later version.
 *
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 */
package org.objectweb.jonas.management;

// general Java imports
import java.util.Hashtable;
import java.util.Properties;

import javax.management.JMException;
import javax.management.MBeanServer;
import javax.management.MBeanServerNotification;
import javax.management.Notification;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.objectweb.jonas.common.JProp;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.jmx.JonasObjectName;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
/**
 * This MBean allows persistent reconfiguration of a JOnAS server, alltogether with its embedded services,
 * and possibly used resources like Data Sources ans Mail Factories.
 * This class implements NotificationListener interface. The ReconfigManager adds itself as Listener to
 * the following JMX Notification types (JAVA types):
 * <ul>
 * <li>MBeanServerNotification</li>, sent by the MBean server on MBeans registration/un-registration
 * <li>Notification with type equal to ReconfigDispatcher.RECONFIG_TYPE</li>
 * <li>Notification with type equal to ReconfigDispatcher.SAVE_RECONFIG_TYPE</li>
 * </ul>
 * @author Adriana Danes
 * 04/09/20 Update with JSR77 JDBCDataSource MBeans
 */
public class ReconfigManager implements ReconfigManagerMBean, NotificationListener {

    /**
     * The logger used in JOnAS
     */
    private static Logger logger = null;

    /**
     * The MBean server attached to the current JOnAS server
     */
    MBeanServer jmxServer = null;

    // The following Strings reflect the current implementation of the org.objectweb.jonas.jmx.JonasObjectName class !!
    // ------------------------------------------------------------------------------------------------------------------
    // The value of the 'type' property in JonasObjectName of the services MBeans
    private static final String SERVICE_TYPE = "service";

    // The value of the 'type' property in JonasObjectName used by DataSources
    private static final String DATASOURCE_TYPE = "datasource";


    // J2EE Management specification conformance
    // -----------------------------------------
    /**
     * The value of the j2eeType key for Mail Resources
     */
    private static final String MAIL_RESOURCE_TYPE = "JavaMailResource";
    /**
     * The value of the j2eeType key for JTA Resources
     */
    private static final String JTA_RESOURCE_TYPE = "JTAResource";
    /**
     * The value of the j2eeType key for JDBCDataSources
     */
    private static final String JDBC_RESOURCE_TYPE = "JDBCDataSource";

    /**
     * Type of the security realm
     */
    private static final String SECURITYREALM_FACTORY = "securityfactory";


    /**
     * Name of the security realm file
     */
    private static final String SECURITYREALM_FILE = "jonas-realm.xml";


    // My JMX name
    ObjectName reconfigManagerObectName = JonasObjectName.serverConfig();

    // The JOnAS server's initialization properties (saves several method call JProp.getInstance().getConfigFileEnv();)
    // and configuration file name
    Properties serverProperties = null;
    String serverConfigFileName = null;

    // Table of Reconfigurator objects.
    // We have a Reconfigurator object per each reconfigurable service + one for the server itself + one per resource
    Hashtable reconfigurators = new Hashtable();

    /**
     * Create the ReconfigManager MBean instance and add itself as listner to MBeanServerNotifications.
     * @param serverProperties The JOnAS server's initial configuration properties
     * @param jmxServer The JOnAS server's MBean server
     */
    public ReconfigManager(Properties serverProperties, MBeanServer jmxServer) throws ReconfigException {
        // get a logger for server traces
        logger = Log.getLogger(Log.JONAS_MANAGEMENT_PREFIX);

        this.serverProperties = serverProperties;
        this.jmxServer = jmxServer;
        try {
            this.serverConfigFileName = JProp.getInstance().getPropFile();
        } catch (Exception e) {
            throw new ReconfigException("Can't initialize ReconfigManager because of exception: " + e.toString());
        }

        // Add myself as listner to  MBeanServerNotifications. Use for this the MBeanServerDelegate ObjectName as
        // argument in the addNotificationListener method (see JMX API for more info).
        // Use null NotificationFilter and null handback object.
        try {
            ObjectName delegate = new ObjectName("JMImplementation:type=MBeanServerDelegate");
            jmxServer.addNotificationListener(delegate, this, null, null);
        } catch (JMException me) {
            // MalformedObjectNameException should not occur if the JMX implementation is correct
            // InstanceNotFoundException should not occur as we use MBeanServerDelegate ObjectName
            throw new ReconfigException("ReconfigManager can't listen to MBeanServerNotifications because of exception: " + me.toString());
        }
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "ReconfigManager MBean registered itself as listner to MBeanServerNotifications");
        }
    }

    /**
     * Treat the notifications emitted by those MBeans having the ReconfigManager added as listener.
     * This method determines the type of the notification and calls the specific treatement.
     * @param notification received notification
     * @param handback received handback object
     */
    public void handleNotification(Notification notification, java.lang.Object handback) {
        String notificationType = notification.getType();
        if (notification instanceof MBeanServerNotification) {
            // This notification is sent by the jmx server. It may be a REGISTRATION_NOTIFICATION or an UNREGISTRATION_NOTIFICATION
            if (notificationType.equals(MBeanServerNotification.REGISTRATION_NOTIFICATION)) {
                try {
                    handleRegistrationNotification((MBeanServerNotification) notification);
                } catch (ReconfigException re) {
                    logger.log(BasicLevel.ERROR, "ReconfigManager error when trying to handle REGISTRATION_NOTIFICATION");
                }
            } else if (notificationType.equals(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Received UNREGISTRATION_NOTIFICATION for MBean " + ((MBeanServerNotification) notification).getMBeanName().toString());
                }
                // TO DO ...
            }
        } else {
            // The MBean sending this notification has an associated Reconfigurator object created by the handleRegistrationNotification() method.
            // Get the Reconfigurator's name from the 'message' field
            String name = notification.getMessage();
            // get the sequence number
            long sequence = notification.getSequenceNumber();
            if (notificationType.equals(ReconfigDispatcher.RECONFIG_TYPE)) {
                // get the reconfigured property (or properties) sent as a UserData within the notification
                Reconfigured  reconfigured = (Reconfigured) notification.getUserData();
                if (reconfigured instanceof ReconfiguredProp) {
                    ReconfiguredProp prop = (ReconfiguredProp) reconfigured;
                        handleReconfig(name, sequence, prop);
                } else {
                    ReconfiguredXml reconfiguredXml = (ReconfiguredXml) reconfigured;
                    handleReconfig(name, sequence, reconfiguredXml);
                }
            } else if (notificationType.equals(ReconfigDispatcher.SAVE_RECONFIG_TYPE)) {
                handleSave(name, sequence);
            }
        }
    }

    /**
     * Treat REGISTRATION_NOTIFICATION methods. If the notification sender is a MBean associated to a service or resources,
     * create a Reconfigurator associated to the sender and adds itself as listener
     * for notifications emitted by the sender.
     * @param notification notification to treat
     * @throws ReconfigException pb. when trying to create reconfiguration support
     */
    private void handleRegistrationNotification(MBeanServerNotification notification) throws ReconfigException {
        ObjectName notificationSender = notification.getMBeanName();
        String senderType = notificationSender.getKeyProperty("type");
        String senderName = notificationSender.getKeyProperty("name");
        String senderJ2eeType = notificationSender.getKeyProperty("j2eeType");
        Reconfigurator reconfig = null;
        if (senderJ2eeType != null) {
            senderType = senderJ2eeType;
        }

        if (senderType == null) {
            // Nothing to do
            return;
        }

        if (senderType.equals(SERVICE_TYPE)) {
            // The registered MBean is a JOnAS service MBean.
            // Create a Reconfigurator for the service (the reconfigurator name is the service name)
            // Currently only support Reconfiguration for JOnAS services
            // (are not supported : the Log system and the server itself)
            if (!senderName.equals("jonasServer")) {
                if (senderName.equals("log")) {
                    // treat log system case
                    String configFileName = notificationSender.getKeyProperty("fname");
                    try {
                        reconfig = new ReconfiguratorProp(senderName, JProp.getInstance(configFileName).getPropFile(), JProp.getInstance(configFileName).getConfigFileEnv());
                    } catch (Exception e) {
                        // This is a FileNotFound exception thrown by JProp.getInstance(resourceName)
                        // which means that the resource was not created from a configuration file at the server start-up.
                        // The persistent configuration of dynamically loaded resource is not implementes yet !!!!!
                        logger.log(BasicLevel.WARN, "Cannot do persistent reconfiguration for dynamically loaded resources!");
                    }
                } else {
                    // treat regular jonas services
                    reconfig = new ReconfiguratorProp(senderName, serverConfigFileName, serverProperties);
                }
            }
        } else {
            // Check if the registered MBean is a JOnAS resource
            // The currently considered resources are Mail Factories and Datasources
            String resourceName = null;
            try {
                if (senderType.equals(MAIL_RESOURCE_TYPE)) {
                    // get the name of the Mail Factory
                    resourceName = (String) jmxServer.getAttribute(notificationSender, "FactoryName");
                } else if (senderType.equals(JDBC_RESOURCE_TYPE)) {
                    // get the name of the Datasource
                    resourceName = (String) jmxServer.getAttribute(notificationSender, "name");
                } else if (senderType.equals(JTA_RESOURCE_TYPE)) {
                    // treat as the regular service case
                    reconfig = new ReconfiguratorProp(senderName, serverConfigFileName, serverProperties);
                    // resource name keeps null so avoids entering in code (if) below
                }

                // xml type
                if (senderType.equals(SECURITYREALM_FACTORY)) {
                    JProp jprop = JProp.getInstance(SECURITYREALM_FILE);
                    String propsFilename = jprop.getPropFile();
                    String txt = jprop.getConfigFileXml();
                    // Create a Reconfigurator for this resource
                    reconfig = new ReconfiguratorXml(SECURITYREALM_FILE, propsFilename, txt);
                } else if (resourceName != null) {
                    JProp jprop = JProp.getInstance(resourceName);
                    String propsFilename = jprop.getPropFile();
                    Properties props = JProp.getInstance(resourceName).getConfigFileEnv();
                    // Create a Reconfigurator for this resource
                    reconfig = new ReconfiguratorProp(resourceName, propsFilename, props);
                }
            } catch (JMException me) {
                logger.log(BasicLevel.ERROR, "Catched Exception when trying to treat reconfiguration of the following resource: " + me);
                throw new ReconfigException("Catched Exception when trying to treat reconfiguration of the following resource: " + me.toString());
            } catch (Exception e) {
                // this exception is thrown by JProp.getInstance(resourceName)
                // its not possible to receive it here because if the MBean registered itself, this call was already exceuted succesfully
                logger.log(BasicLevel.ERROR, "Catched Exception when calling JProp.getInstance(" + resourceName + "): " + e);
                throw new ReconfigException("Catched Exception when calling JProp.getInstance(" + resourceName + "): " + e.toString());
            }
        }
        if (reconfig != null) {
            try {
                jmxServer.addNotificationListener(notificationSender, reconfigManagerObectName, null, null);
            } catch (JMException me) {
                logger.log(BasicLevel.ERROR, "ReconfigManager can't listen to Notifications because of exception: " + me.toString());
                throw new ReconfigException("ReconfigManager can't listen to Notifications because of exception: " + me.toString());
            }
            reconfigurators.put(senderName, reconfig);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Received Registration Notification from " + notificationSender.toString());
            }
        }
    }

    /**
     * Treat reconfiguration operation by calling the specific method on the associated reconfigurator
     * @param name reconfigurator name
     * @param sequence sequence number of the reconfiguration notification to treat
     * @param prop reconfigured property (or properties)
     */
    private void handleReconfig(String name, long sequence, ReconfiguredProp prop) throws ReconfigException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Received 'jonas.management.reconfiguration' notification concerning service or ressource: " +  name);
        }
        Reconfigurator reconfigurator = (ReconfiguratorProp) reconfigurators.get(name);
        if (reconfigurator == null) {
            throw new ReconfigException("Can't find Reconfigurator associated to service or resource " + name);
        } else {
           if (prop.getPropValue() != null) {
               if (logger.isLoggable(BasicLevel.DEBUG)) {
                   logger.log(BasicLevel.DEBUG, "Try to reconfigure property : " + prop.getPropName() + " using value value : " + prop.getPropValue());
               }
                if (prop.replaceProp()) {
                    ((ReconfiguratorProp) reconfigurator).updateConfig(prop.getPropName(), prop.getPropValue(), sequence);
                } else {
                    if (logger.isLoggable(BasicLevel.DEBUG)) {
                        if (prop.addProp()) {
                            logger.log(BasicLevel.DEBUG, "This value has to be added to the values sequence");
                        } else {
                            logger.log(BasicLevel.DEBUG, "This value has to be removed from the values sequence");
                        }
                    }
                    ((ReconfiguratorProp) reconfigurator).updateConfig(prop.getPropName(), prop.getPropValue(), prop.addProp(), sequence);
                }
            } else {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Reconfiguration made on a group of properties");
                }
                ((ReconfiguratorProp) reconfigurator).updateConfig(prop.getProps(), sequence);
            }
        }
    }

    /**
     * Treat reconfiguration operation by calling the specific method on the associated reconfigurator
     * @param name reconfigurator name
     * @param sequence sequence number of the reconfiguration notification to treat
     * @param reconfiguredXml reconfigured xml
     */
    private void handleReconfig(String name, long sequence, ReconfiguredXml reconfiguredXml) throws ReconfigException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Received 'jonas.management.reconfiguration' notification concerning service or ressource: " +  name);
        }
        Reconfigurator reconfigurator = (ReconfiguratorXml) reconfigurators.get(name);
        if (reconfigurator == null) {
            throw new ReconfigException("Can't find Reconfigurator associated to service or resource " + name);
        } else {
                ((ReconfiguratorXml) reconfigurator).updateConfig(reconfiguredXml.getXml(), sequence);
        }
    }


    /**
     * Treat save operation by calling the specific method on the associated reconfigurator
     * @param name reconfigurator name
     * @param sequence sequence number of the reconfiguration notification to treat
     */
    private void handleSave(String name, long sequence) throws ReconfigException {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Received 'jonas.management.reconfiguration.save' notification concerning service or ressource: " + name);
        }
        Reconfigurator reconfigurator = (Reconfigurator) reconfigurators.get(name);
        if (reconfigurator == null) {
            throw new ReconfigException("Can't find Reconfigurator associated to service or resource " + name);
        } else {
            reconfigurator.saveConfig(sequence);
        }
    }
}
