/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2008 Bull S.A.S.
 * Contact: jonas-team@ow2.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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.mail.internal;


import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;

import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.lib.bootstrap.JProp;
import org.ow2.jonas.lib.execution.ExecutionResult;
import org.ow2.jonas.lib.execution.IExecution;
import org.ow2.jonas.lib.execution.RunnableHelper;
import org.ow2.jonas.lib.management.javaee.J2eeObjectName;
import org.ow2.jonas.lib.service.AbsServiceImpl;
import org.ow2.jonas.lib.util.JModule;
import org.ow2.jonas.lib.util.JonasObjectName;
import org.ow2.jonas.lib.util.Log;
import org.ow2.jonas.lib.util.PropDump;
import org.ow2.jonas.mail.MailService;
import org.ow2.jonas.mail.MailServiceException;
import org.ow2.jonas.mail.factory.JavaMail;
import org.ow2.jonas.mail.internal.factory.JavaMailMimePartDS;
import org.ow2.jonas.mail.internal.factory.JavaMailSession;
import org.ow2.jonas.mail.internal.factory.mbean.JavaMailMimePartDSResource;
import org.ow2.jonas.mail.internal.factory.mbean.JavaMailSessionResource;
import org.ow2.jonas.registry.RegistryService;
import org.ow2.jonas.service.ServiceException;

/**
 * This class provides an implementation of the javaMail service.
 * @author Florent Benoit
 * @author Ludovic Bert
 * Contributor(s):
 * Adriana Danes :
 * - Make possible to change configuration of a JOnAS mail factory object.
 * - Make possible to change the JNDI name of a JOnAS mail factory object.
 */
public class JOnASMailService extends AbsServiceImpl implements MailService {

    /**
     * Logger for this service.
     */
    private static Logger logger = Log.getLogger(Log.JONAS_MAIL_PREFIX);

    /**
     * Reference to the JmXService.
     */
    private JmxService jmxService = null;

    /**
     * Initial Context for Naming.
     */
    private Context ictx = null;

    /**
     * Registry Service.
     */
    private RegistryService registryService;

    /**
     * List of the factories names to load when starting the MailService.
     */
    private List<String> factoryNames = new Vector<String>();

    /**
     *  List of the javax.mail.Session factories currently loaded.
     */
    private Map<String, JavaMailSession> jMailSessionFactories = new Hashtable<String, JavaMailSession>();

    /**
     *  List of the javax.mail.internet.MimePartDataSource factories currently loaded.
     */
    private Map<String, JavaMailMimePartDS> jMailMimePartDSFactories = new Hashtable<String, JavaMailMimePartDS>();

    /**
     *  List of the bound factories (jndi name -> factory name).
     *  This is needed by EJB components in order to find out the factory name
     *  based upon their jndi name
     */
    private Map<String, String> boundFactories = new Hashtable<String, String>();

    /**
     * Session Factory.
     */
    private static final int JAVAX_MAIL_SESSION_FACTORY = 1;

    /**
     * MimepartDatasource Factory.
     */
    private static final int JAVAX_MAIL_INTERNET_MIMEPARTDATASOURCE = 2;

    /**
     * Mail service configuration parameters (factories).
     */
    public static final String FACTORIES = "jonas.service.mail.factories";

    /**
     * Mail service configuration parameters (class).
     */
    public static final String CLASS = "jonas.service.mail.class";

    /**
     * @param factories initial (comma separated list) of mail factories.
     */
    public void setFactories(final String factories) {
        factoryNames = convertToList(factories);
    }

    /**
     * Start the Mail Service.
     * @throws ServiceException if the initialization failed.
     */
    @Override
    protected void doStart() throws ServiceException {

        // the logger for reconfig management
        //super.initLogger(Log.getLogger(Log.JONAS_MANAGEMENT_PREFIX));
        // Get the InitialContext in an execution block
        IExecution<InitialContext> ictxGetter = new IExecution<InitialContext>() {
            public InitialContext execute() throws Exception {
                return getRegistryService().getRegistryContext();
            }
        };

        // Execute
        ExecutionResult<InitialContext> ictxResult = RunnableHelper.execute(getClass().getClassLoader(), ictxGetter);
        // Throw an ServiceException if needed
        if (ictxResult.hasException()) {
            logger.log(BasicLevel.ERROR, "Cannot create initial context when Mail service initializing");
            throw new ServiceException("Cannot create initial context when Mail service initializing",
                                       ictxResult.getException());
        }
        // Store the ref
        ictx = ictxResult.getResult();

        // Load mbeans-descriptor.xml into the modeler registry
        // Needs to be done before creating Mail Factories.
        // TODO, When mail service will become a bundle, we could use
        // the meta-data located in META-INF/mbeans-descriptors.xml
        jmxService.loadDescriptors(getClass().getPackage().getName(), getClass().getClassLoader());

        // creates each factory
        String factoryName = null;
        for (int i = 0; i < factoryNames.size(); i++) {
            factoryName = factoryNames.get(i);
            try {
                JProp prop = JProp.getInstance(factoryName);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Creating mail factory " + factoryName);
                }
                createMailFactory(factoryName, prop.getConfigFileEnv());
            } catch (Exception e) {
                if (logger.isLoggable(BasicLevel.ERROR)) {
                    logger.log(BasicLevel.ERROR, "JOnAS: Cannot create mail factory " + factoryName + " : " + e);
                    logger.log(BasicLevel.ERROR, "Please check the " + factoryName + ".properties file");
                }
            }
        }

        // Register MailService MBean
        try {
            jmxService.registerModelMBean(this, JonasObjectName.mailService(getDomainName()));
        } catch (Exception e) {
            // Continue the execution ...
            logger.log(BasicLevel.INFO, "Cannot register 'mail' service MBean", e);
        }
        logger.log(BasicLevel.INFO, "Mail Service started");
    }

    /**
     * Stop the Mail service.
     * @throws ServiceException if the stop failed.
     */
    @Override
    protected void doStop() throws ServiceException {

        try {
            unbindMailFactories();
        } catch (MailServiceException e) {
            logger.log(BasicLevel.ERROR, "Cannot unbind mail factories " + e);
            throw new ServiceException("Cannot unbind mail factories ", e);
        }

        // Unregister the Service MBean
        if (jmxService != null) {
            jmxService.unregisterModelMBean(JonasObjectName.mailService(getDomainName()));
            // TODO We should remove the Model MBeans loaded by the JMX Service
        }
        logger.log(BasicLevel.INFO, "mail service stopped");
    }

    /**
     * This method is used when a Mail Factory configuration is modified via jonasAdmin.
     * In this case, the updated JavaMail object (JavaMailSession or JavaMailMimePartDS object)
     * must be rebound in JNDI
     * @param factory the factory
     * @throws MailServiceException if the recreation of
     * the factory failed.
     */
    public void recreateJavaMailFactory(final JavaMail factory) throws MailServiceException {
        String jndiName = factory.getName();
        // Rebind the factory object in the naming context
        try {
            ictx.rebind(jndiName, factory);
        } catch (NamingException e) {
            logger.log(BasicLevel.ERROR, "Cannot bind mail factory '" + jndiName + "'  :" + e.getMessage());
            throw new MailServiceException("Cannot bind mail factory " + jndiName + ".", e);
        }
    }

    /**
     * This method is used when a particular Mail Factory configuration operation is done via jonasAdmin :
     * when the JNDI name of this resource is modified.
     * In this case, the initial JavaMail object (JavaMailSession or JavaMailMimePartDS object) must be unbound
     * and the updated JavaMail object must be reloaded.
     * Also, the Mail Service private data structures must be updated.
     * @param oldName old name of the factory
     * @param factory the new factory
     * @throws MailServiceException if the rename of the
     * the factory failed.
     */
    public void renameJavaMailFactory(final String oldName, final JavaMail factory) throws MailServiceException {

        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "In renameMailFactory, old name = " + oldName);
        }
        try {
            ictx.unbind(oldName);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, oldName + " unbound");
            }
        } catch (Exception e) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Warning: cannot unbind mail factory object named " + oldName);
            }
        }
        String jndiName = factory.getName();
        // Rebind the factory object in the naming context
        try {
            ictx.rebind(jndiName, factory);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "factory rebound under the name " + jndiName);
            }
        } catch (NamingException e) {
            logger.log(BasicLevel.ERROR, "Cannot bind mail factory '" + jndiName + "'  :" + e.getMessage());
            throw new MailServiceException("Cannot bind mail factory " + jndiName + ".", e);
        }
        boundFactories.put(jndiName, factory.getFactoryName());
        boundFactories.remove(oldName);
    }

    /**
     * Create a mail factory with the specified properties and register it
     * into the registry.
     * @param factoryName name of the factory to create
     * @param props the properties used to configure the mail factory.
     * @throws MailServiceException if the creation or the registration of
     * the factory failed.
     */
    public void createMailFactory(final String factoryName, final Properties props)
        throws MailServiceException {

        if (logger.isLoggable(BasicLevel.DEBUG)) {
            PropDump.print("These are the properties from which the MailService picks to construct Mail Factories",
                           props,
                           logger,
                           BasicLevel.DEBUG);
        }

        Object factory = null;

        //Factory type/jndi name must be non null
        String factoryType = props.getProperty(PROPERTY_TYPE);
        String jndiName = props.getProperty(PROPERTY_NAME);

        if (jndiName == null) {
            logger.log(BasicLevel.ERROR, "The property 'mail.factory.name' is a required property.");
            throw new MailServiceException("The property 'mail.factory.name' is a required property for this factory.");
        }

        if (factoryType == null) {
            logger.log(BasicLevel.ERROR, "The property 'mail.factory.type' is a required property.");
            throw new MailServiceException("The property 'mail.factory.type' is a required property for this factory.");
        }

        // Verify that jndi name not already used
        if (boundFactories.containsKey(jndiName)) {
            logger.log(BasicLevel.ERROR, "There is already a factory bound with the name " + jndiName);
            throw new MailServiceException("There is already a factory bound with the name '"
                                           + jndiName +  "', please correct the provided configuration properties");
        }

        //Define the type of our factory
        int typeOfFactory;
        if (factoryType.equalsIgnoreCase("javax.mail.Session")) {
            typeOfFactory = JAVAX_MAIL_SESSION_FACTORY;
        } else if (factoryType.equalsIgnoreCase("javax.mail.internet.MimePartDataSource")) {
            typeOfFactory = JAVAX_MAIL_INTERNET_MIMEPARTDATASOURCE;
        } else {
            typeOfFactory = 0;
        }

        // Create the factory object and register it in the internal data structure
        switch (typeOfFactory) {

        case JAVAX_MAIL_SESSION_FACTORY :
            JavaMailSession sessionFactory = new JavaMailSession(factoryName, jndiName, props, this);
            jMailSessionFactories.put(factoryName, sessionFactory);
            factory = sessionFactory;
            break;

        case JAVAX_MAIL_INTERNET_MIMEPARTDATASOURCE :
            JavaMailMimePartDS mimeFactory = new JavaMailMimePartDS(factoryName, jndiName, props, this);
            jMailMimePartDSFactories.put(factoryName, mimeFactory);
            factory = mimeFactory;
            break;

        default :
            throw new MailServiceException("Can not create a factory of the type '" + factoryType + "'. This type is incorrect.");
        }

        // Bind the factory object in the naming context
        try {
            ictx.rebind(jndiName, factory);
            boundFactories.put(jndiName, factoryName);
        } catch (NamingException e) {
            logger.log(BasicLevel.ERROR, "Cannot bind mail factory '" + jndiName + "'  :" + e.getMessage());
            throw new MailServiceException("Cannot bind mail factory " + jndiName + ".", e);
        }

        logger.log(BasicLevel.INFO, "Mapping Mail Factory " + factoryType + " on " + jndiName);

        // Register the factory object as an MBean with the jmx server
        try {
            ObjectName on = null;
            switch (typeOfFactory) {
            case JAVAX_MAIL_SESSION_FACTORY :
                // J2EEManagement

                on = J2eeObjectName.JavaMailResource(getDomainName(),
                                                     factoryName,
                                                     getJonasServerName(),
                                                     SESSION_PROPERTY_TYPE);
                JavaMailSessionResource javaMailSessionResource = null;
                javaMailSessionResource = new JavaMailSessionResource(on.toString(),
                                                                      false,
                                                                      false,
                                                                      false,
                                                                      (JavaMailSession) factory);
                // JSR77
                jmxService.registerModelMBean(javaMailSessionResource, on);
                //mbeanServer.registerMBean(s_mbean, on);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Register session mail factory with name " + factoryName);
                }
                break;
            case JAVAX_MAIL_INTERNET_MIMEPARTDATASOURCE :
                // J2EEManagement
                on = J2eeObjectName.JavaMailResource(getDomainName(),
                                                     factoryName,
                                                     getJonasServerName(),
                                                     MIMEPART_PROPERTY_TYPE);
                JavaMailMimePartDSResource javaMailMimePartDSResource = null;
                javaMailMimePartDSResource = new JavaMailMimePartDSResource(on.toString(),
                                                                            false,
                                                                            false,
                                                                            false,
                                                                            (JavaMailMimePartDS) factory);
                // JSR77
                jmxService.registerModelMBean(javaMailMimePartDSResource, on);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Register mime mail factory with name " + factoryName);
                }
            }
        } catch (Exception e) {
            logger.log(BasicLevel.WARN, "Could not register JavaMailResource MBean");
        }


    }

    /**
     * Create a mail factory with the specified properties and register it
     * into the registry.
     * @param name the mail factory name
     * @param props the properties used to configure the mail factory.
     * @param loadFromFile true if the mail factory is loaded from a .properties file
     * @throws MailServiceException if the creation or the registration of
     * the factory failed.
     * @throws MailServiceException
     */
    public void createMailFactoryMBean(final String name, final Properties props, final Boolean loadFromFile)
        throws MailServiceException {

        boolean fromFile = loadFromFile.booleanValue();
        if (!fromFile) {
            try {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Call getInstance on JProp in order to create the properties file");
                }
                JProp.getInstance(name, props);
            } catch (Exception e) {
                logger.log(BasicLevel.ERROR,
                           "Cannot create mail factory " + name + " as cannot create properties file : " + e.toString());
                throw new ServiceException("MailService: Cannot create mail factory " + name + ",\n" + e.toString());
            }
        }
        //call the internal proc
        try {
            createMailFactory(name, props);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Cannot create mail factory: " + name);
            throw new ServiceException("MailService: Cannot create mail factory: " + name + ",\n" + e.toString());
        }
    }

    /**
     * Unregister all the binding factories on the server and in JMX Server.
     * @throws MailServiceException if the unregistration of the factories
     * failed.
     */
    public void unbindMailFactories() throws MailServiceException {
        //Unbind all factories
        // Need to store the keys, because we will change the Maps in unbindMailFactoryMBean
        // This would throw a ConcurrentModificationException if we were directly iterating the Map
        String[] factories = new String[jMailSessionFactories.size()];
        factories = jMailSessionFactories.keySet().toArray(factories);

        for (int i = 0; i < factories.length; i++) {
            String factoryName = factories[i];
            unbindMailFactoryMBean(factoryName);
        }

        factories = new String[jMailMimePartDSFactories.size()];
        factories = jMailMimePartDSFactories.keySet().toArray(factories);
        for (int i = 0; i < factories.length; i++) {
            String factoryName = factories[i];
            unbindMailFactoryMBean(factoryName);
        }
    }

    /**
     * Unregister the factory with the given name.
     * @param factoryName the name of the factory to unbind.
     * @throws MailServiceException if the unregistration of the factory
     * failed.
     */
    public void unbindMailFactory(final String factoryName)
        throws MailServiceException {
        unbindMailFactoryMBean(factoryName);
    }

    /**
     * Unregister the factory with the given name.
     * @param factoryName the name of the factory to unbind.
     * @throws MailServiceException if the unregistration of the factory
     * failed.
     */
    public void unbindMailFactoryMBean(final String factoryName)
        throws MailServiceException {

        // determine the type of the factory and the jndi name
        String name = null;
        int typeOfFactory;
        JavaMailSession jmailSession = jMailSessionFactories.get(factoryName);
        JavaMailMimePartDS jMailMimePartDS = jMailMimePartDSFactories.get(factoryName);
        if (jmailSession != null) {
            name = jmailSession.getName();
            typeOfFactory = JAVAX_MAIL_SESSION_FACTORY;
        } else if (jMailMimePartDS != null) {
            name = jMailMimePartDS.getName();
            typeOfFactory = JAVAX_MAIL_INTERNET_MIMEPARTDATASOURCE;
        } else {
            throw new MailServiceException("Can not unload the mail factory '"
                                           + factoryName + "' (this is not a known factory name)");
        }

        // unbind the factory
        try {
            ictx.unbind(name);
            boundFactories.remove(name);
        } catch (NamingException e) {
            throw new MailServiceException("Can not unbind the factory '" + name + "'.", e);
        }

        // remove JProp instance corresponding to this datasource in order to
        // allow re-create in case its re-loaded
        JProp.removeInstance(name);

        // De-register the factory object from the jmx server and remove the factory from the internal data structure
        if (jmxService != null) {
            ObjectName on = null;
            switch (typeOfFactory) {
            case JAVAX_MAIL_SESSION_FACTORY :
                // J2EEManagement
                on = J2eeObjectName.JavaMailResource(getDomainName(),
                                                     factoryName,
                                                     getJonasServerName(),
                                                     SESSION_PROPERTY_TYPE);
                jmxService.unregisterModelMBean(on);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Unregister session mail factory with name " + factoryName);
                }
                jMailSessionFactories.remove(factoryName);
                break;
            case JAVAX_MAIL_INTERNET_MIMEPARTDATASOURCE :
                // J2EEManagement
                on = J2eeObjectName.JavaMailResource(getDomainName(),
                                                     factoryName,
                                                     getJonasServerName(),
                                                     MIMEPART_PROPERTY_TYPE);
                jmxService.unregisterModelMBean(on);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Unregister mime mail factory with name " + factoryName);
                }
                jMailMimePartDSFactories.remove(factoryName);
            }
        }

    }

    /**
     * Gets the factory name given the jndi name. Null is returned if the given name is not bound.
     * @param jndiName the jndi name
     * @return the factory name given the jndi name. Null is returned if the given name is not bound.
     */
    public String getFactoryName(final String jndiName) {
        return boundFactories.get(jndiName);
    }

    /**
     * Gets the total number of mail factories available in JOnAS.
     * @return Integer Total number of mail factories available in JOnAS
     */
    public Integer getCurrentNumberOfMailFactories() {
        return Integer.valueOf(jMailSessionFactories.size() + jMailMimePartDSFactories.size());
    }

    /**
     * Gets the number of Session mail factories available in JOnAS.
     * @return Integer Number of Session mail factories available in JOnAS
     */
    public Integer getCurrentNumberOfSessionMailFactories() {
        return Integer.valueOf(jMailSessionFactories.size());
    }

    /**
     * Gets the integer Number of internet.
     * @return Integer Number of internet.MimePartDataSource mail factories available in JOnAS
     */
    public Integer getCurrentNumberOfMimeMailFactories() {
        return Integer.valueOf(jMailMimePartDSFactories.size());
    }

    /**
     * Gets the mail factory configuration properties from a local file.
     * @param configFile configuration to use
     * @return mail factory configuration properties from a local file
     * @throws Exception if it fails
     */
    public Properties getMailFactoryPropertiesFile(final String configFile) throws Exception {
        try {
            return JProp.getInstance(configFile).getConfigFileEnv();
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Error occured when reading file " + configFile);
            throw e;
        }
    }

    /**
     * MBean method.
     * @return the list of properties files describing mail factories found in JONAS_BASE/conf
     */
    public List<String> getMailFactoryPropertiesFiles() throws Exception {
        return JModule.getMailFactoryPropsInDir();
    }

    /**
     * MBean method.
     * @return the list of properties files describing mail factories found in JONAS_BASE/conf
     */
    public List<String> getMimePartMailFactoryPropertiesFiles() throws Exception {
        return JModule.getMailFactoryPropsInDir(MIMEPART_PROPERTY_TYPE);
    }

    /**
     * MBean method.
     * @return the list of properties files describing mail factories found in JONAS_BASE/conf
     */
    public List<String> getSessionMailFactoryPropertiesFiles() throws Exception {
        return JModule.getMailFactoryPropsInDir(SESSION_PROPERTY_TYPE);
    }

    /**
     * @param jmxService the jmxService to set
     */
    public void setJmxService(final JmxService jmxService) {
        this.jmxService = jmxService;
    }

    /**
     * @param registry the registry service to set
     */
    public void setRegistryService(final RegistryService registry) {
        this.registryService = registry;
    }

    /**
     * Returns the registry service.
     * @return The registry service
     */
    private RegistryService getRegistryService() {
        return registryService;
    }
}
