/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.security;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.Reader;
import java.io.StringReader;
import java.security.NoSuchAlgorithmException;

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

import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.jmx.JmxService;
import org.objectweb.jonas.jmx.JonasObjectName;
import org.objectweb.jonas.security.lib.wrapper.JResourceManagerWrapper;
import org.objectweb.jonas.security.realm.factory.JResource;
import org.objectweb.jonas.security.realm.factory.JResourceDS;
import org.objectweb.jonas.security.realm.factory.JResourceLDAP;
import org.objectweb.jonas.security.realm.factory.JResourceMemory;
import org.objectweb.jonas.security.realm.factory.JResourceRemote;
import org.objectweb.jonas.security.realm.factory.JResourceRemoteImpl;
import org.objectweb.jonas.security.realm.lib.HashHelper;
import org.objectweb.jonas.service.AbsServiceImpl;
import org.objectweb.jonas.service.ServiceException;
import org.objectweb.jonas.service.ServiceManager;

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

/**
 * Security Service implementation
 * @author Jeff Mesnil,Philippe Coq, John Ellis, Joe Gittings for old security
 *         service
 * @author Florent Benoit - JOnAS 3.x (Add JResources) - JOnAS 4.x (remove
 *         MethodGuard, RoleGuard no more used with JACC)
 */

public class JonasSecurityServiceImpl extends AbsServiceImpl implements SecurityService, JonasSecurityServiceImplMBean {

    /**
     * Logger which is used
     */
    private static Logger logger = null;

    /**
     * Security service configuration properties
     */
    public static final String CLASS = "jonas.service.security.class";

    /**
     * Name of resource
     */
    public static final String REMOTE_RESOUCE = "_remoteres";

    /**
     * Relative path of the realm configuration file
     */
    protected static final String CONFIG_FILE = "conf" + File.separator + "jonas-realm.xml";

    /**
     * Bind the resources into JNDI ?
     */
    protected static final String BIND_RESOURCES_INTO_JNDI = "jonas.service.security.realm.jndi.registration";

    /**
     * Reference to a MBean server.
     */
    private MBeanServer mbeanServer = null;

    /**
     * JResources list
     */
    private JResources jResources;

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

    /**
     * Bind resource in JNDI
     */
    private boolean bindResourcesIntoJndi = false;


    /**
     * Init the Service. Configuration information is passed thru a Context
     * object.
     * @param ctx the configuration of the Security service.
     * @throws ServiceException if the initialization failed.
     */
    public void doInit(Context ctx) throws ServiceException {
        if (logger == null) {
            logger = Log.getLogger(Log.JONAS_SECURITY_PREFIX);
        }

        // JResources
        jResources = new JResources(this);

        // Get the initial Context
        try {
            ictx = new InitialContext();
        } catch (NamingException e) {
            logger.log(BasicLevel.ERROR, "Cannot create initial context during the mail service initializing", e);
            throw new ServiceException("Cannot create initial context during the mail service initializing", e);
        }

        // Register the security remote object (name is based on JOnAS server
        // name).
        JResourceRemote jResourceRemote = null;
        try {
            jResourceRemote = new JResourceRemoteImpl();
            ictx.rebind(getJonasServerName() + REMOTE_RESOUCE, jResourceRemote);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Cannot bind remote resource for security access", e);
            throw new ServiceException("Cannot bind remote resource for security access", e);
        }

        // Get the JMX Server via JMX Service
        try {
            mbeanServer = ((JmxService) ServiceManager.getInstance().getJmxService()).getJmxServer();
        } catch (Exception e) {
            // the JMX service may not be started
            mbeanServer = null;
        }

        try {
            String s = (String) ctx.lookup(BIND_RESOURCES_INTO_JNDI);
            bindResourcesIntoJndi = new Boolean(s).booleanValue();
        } catch (Exception e) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Property '" + BIND_RESOURCES_INTO_JNDI + "' not available, set it to false by default", e);
            }
        }


        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "JonasSecurityService initialized");
        }
    }

    /**
     * Remove the Resource (memory, ldap, datasource,...)
     * @param resourceName name of the resource
     * @throws Exception if the resource name does not exist
     */
    public void removeJResource(String resourceName) throws Exception {

        // remove the given resource of the list
        JResource jResource = jResources.remove(resourceName);

        // remove the resource into the jndi
        if (bindResourcesIntoJndi) {
            try {
                ictx.unbind(resourceName);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "jResource " + resourceName + " remove from the registry.");
                }
            } catch (NamingException e) {
                logger.log(BasicLevel.ERROR, "Cannot unbind the resource '" + resourceName + "' into JNDI", e);
            }
        }

        try {
            // Remove mbeans of the resources
            jResource.removeMBeans();

            // register security factory mbean
            if (jResource instanceof JResourceMemory) {
                mbeanServer.unregisterMBean(JonasObjectName.securityMemoryFactory(resourceName));
            } else if (jResource instanceof JResourceDS) {
                mbeanServer.unregisterMBean(JonasObjectName.securityDatasourceFactory(resourceName));
            } else if (jResource instanceof JResourceLDAP) {
                mbeanServer.unregisterMBean(JonasObjectName.securityLdapFactory(resourceName));
            }
        } catch (ServiceException se) {
            logger.log(BasicLevel.ERROR, "JMX service not available", se);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Can not unregister the MBean for the resource " + resourceName + " : "
                    + e.getMessage());
            throw new ServiceException("Can not unregister the MBean for the resource " + resourceName + " : "
                    + e.getMessage());
        }

    }

    /**
     * Start the Service Initialization of the service is already done.
     * @throws ServiceException if the stop failed.
     */
    public void doStart() throws ServiceException {
        try {
            // register security service mbean
            mbeanServer.registerMBean(this, JonasObjectName.securityService());
        } catch (ServiceException se) {
            logger.log(BasicLevel.ERROR, "JMX service not available", se);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "SecurityService: Cannot start the Security service:\n" + e);
            throw new ServiceException("SecurityService: Cannot start the Security service", e);
        }

        createRealm();
    }

    /**
     * Stop the Service
     */
    public void doStop() {

        // Unregister MBean
        try {
            // Unregister Security Service MBean : SecurityServiceImplMBean
            ((JmxService) ServiceManager.getInstance().getJmxService()).getJmxServer().unregisterMBean(
                    JonasObjectName.securityService());
        } catch (ServiceException se) {
            logger.log(BasicLevel.ERROR, "JMX service not available", se);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Cannot stop the security service:\n" + e);
            throw new ServiceException("Cannot stop the security service", e);
        }


        // Unregister the security remote object (name is based on JOnAS server
        // name).
        try {
            ictx.unbind(getJonasServerName() + REMOTE_RESOUCE);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Cannot unbind remote resource for security access", e);
            throw new ServiceException("Cannot unbind remote resource for security access", e);
        }
    }

    /**
     * Return a resource by giving its name
     * @param name the wanted Resource
     * @return a JResouce
     */
    public JResource getJResource(String name) {
        return jResources.getJResource(name);
    }

    /**
     * Parse the xml file and create all the JResource
     * @throws ServiceException if a JResource can't be created
     */
    private void createRealm() throws ServiceException {

        // Execute the digester for the parsing of the jonas-realm.xml file.
        File configFile = null;
        Reader reader = null;
        try {
            configFile = getConfigFile();
            reader = new FileReader(configFile);
        } catch (FileNotFoundException e) {
            logger.log(BasicLevel.ERROR, "Cannot find config file " + configFile);
            throw new ServiceException(e.getMessage(), e);
        }

        try {
            JResourceManagerWrapper.addResources(jResources, reader, configFile.getPath());
        } catch (Exception e1) {
            String err = "Cannot add security resource from '" + configFile + "'";
            logger.log(BasicLevel.ERROR, err);
            throw new ServiceException(err, e1);
        }
    }

    /**
     * Return a File object representing the jonas-realm.xml configuration file.
     * @return a File object representing the jonas-realm.xml configuration
     *         file.
     * @throws FileNotFoundException if the configuration file is not found.
     */
    protected File getConfigFile() throws FileNotFoundException {
        String fileName = System.getProperty("jonas.base");
        fileName = fileName + File.separator + CONFIG_FILE;
        File file = new File(fileName);
        if (!file.exists()) {
            String err = "Can't find configuration file : " + fileName;
            throw new FileNotFoundException(err);
        }
        return (file);
    }

    /**
     * String representation of the JOnAS realm
     * @return the xml representation of the JOnAS realm
     */
    public String toXML() {
        return jResources.toXML();
    }

    /**
     * Encrypt a string with an algorithm
     * @param string the string to encode
     * @param algo algorithm to apply on the given string
     * @return the encoded string
     * @throws NoSuchAlgorithmException One reason could be a bad algorithm
     */
    public String encryptPassword(String string, String algo) throws NoSuchAlgorithmException {
        String encrypt = HashHelper.hashPassword(string, algo);
        // Prefix with algorithm
        return "{" + algo.toUpperCase() + "}" + encrypt;
    }

    /**
     * Check if the given algorithm is a valid algorithm
     * @param algo algorithm to apply on the given string
     * @return true if it is a valid algorithm
     */
    public boolean isValidAlgorithm(String algo) {
        boolean b = true;
        try {
            encryptPassword("test", algo);
        } catch (NoSuchAlgorithmException nsae) {
            b = false;
        }
        return b;
    }

    /**
     * Add JResources with a given xml configuration
     * @param xml xml representation of the resources to add
     * @throws Exception if the resources can't be added
     */
    public void addResources(String xml) throws Exception {

        try {
            JResourceManagerWrapper.addResources(jResources, new StringReader(xml), "");
        } catch (Exception e1) {
            String err = "Cannot add security resource from xml '" + xml + "'";
            logger.log(BasicLevel.ERROR, err);
            throw new ServiceException(err, e1);
        }
    }

    /**
     * Add a Memory resource
     * @param name the name of the JResourceMemory to create
     * @throws Exception if the resource can't be added
     */
    public void addJResourceMemory(String name) throws Exception {

        // Build a new JResourceMemory
        JResourceMemory jResourceMemory = new JResourceMemory();
        jResourceMemory.setName(name);

        // Build xml
        StringBuffer xml = new StringBuffer(JResources.HEADER_XML);
        xml.append("<jonas-realm>");
        xml.append("<jonas-memoryrealm>");
        xml.append(jResourceMemory.toXML());
        xml.append("</jonas-memoryrealm>");
        xml.append("</jonas-realm>");

        // Add the resource
        addResources(xml.toString());

    }

    /**
     * Add a DS resource
     * @param name the name of the JResourceDS to create
     * @param dsName Name of the datasource resource to use.
     * @param userTable Name of table which have the username/password
     * @param userTableUsernameCol Column of the username of the user table
     * @param userTablePasswordCol Column of the password of the user table
     * @param roleTable Name of table which have the username/role
     * @param roleTableUsernameCol Column of the username of the role table
     * @param roleTableRolenameCol Column of the role of the role table
     * @param algorithm Default algorithm. If specified, the default is not
     *        'clear' password
     * @throws Exception if the resource can't be added
     */
    public void addJResourceDS(String name, String dsName, String userTable, String userTableUsernameCol,
            String userTablePasswordCol, String roleTable, String roleTableUsernameCol, String roleTableRolenameCol,
            String algorithm) throws Exception {

        // Build a new JResourceDS
        JResourceDS jResourceDS = new JResourceDS();
        jResourceDS.setName(name);
        jResourceDS.setDsName(dsName);
        jResourceDS.setUserTable(userTable);
        jResourceDS.setUserTableUsernameCol(userTableUsernameCol);
        jResourceDS.setUserTablePasswordCol(userTablePasswordCol);
        jResourceDS.setRoleTable(roleTable);
        jResourceDS.setRoleTableUsernameCol(roleTableUsernameCol);
        jResourceDS.setRoleTableRolenameCol(roleTableRolenameCol);
        jResourceDS.setAlgorithm(algorithm);

        // Build xml
        StringBuffer xml = new StringBuffer(JResources.HEADER_XML);
        xml.append("<jonas-realm>");
        xml.append("<jonas-dsrealm>");
        xml.append(jResourceDS.toXML());
        xml.append("</jonas-dsrealm>");
        xml.append("</jonas-realm>");

        // Add the resource
        addResources(xml.toString());

    }

    /**
     * Add a LDAP resource
     * @param name the name of the JResourceLDAP to create
     * @param initialContextFactory Initial context factory for the LDAp server
     * @param providerUrl Url of the ldap server
     * @param securityAuthentication Type of the authentication used during the
     *        authentication to the LDAP server
     * @param securityPrincipal DN of the Principal(username). He can retrieve
     *        the information from the user
     * @param securityCredentials Credential(password) of the principal
     * @param securityProtocol Constant that holds the name of the environment
     *        property for specifying the security protocol to use.
     * @param language Constant that holds the name of the environment property
     *        for specifying the preferred language to use with the service.
     * @param referral Constant that holds the name of the environment property
     *        for specifying how referrals encountered by the service provider
     *        are to be processed.
     * @param stateFactories Constant that holds the name of the environment
     *        property for specifying the list of state factories to use.
     * @param authenticationMode Mode for validate the authentication
     *        (BIND_AUTHENTICATION_MODE or COMPARE_AUTHENTICATION_MODE)
     * @param userPasswordAttribute Attribute in order to get the password from
     *        the ldap server
     * @param userRolesAttribute Attribute in order to get the user role from
     *        the ldap server
     * @param roleNameAttribute Attribute for the role name when performing a
     *        lookup on a role
     * @param baseDN DN used for the lookup
     * @param userDN DN used when searching the user DN. Override the baseDN if
     *        it is defined
     * @param userSearchFilter Filter used when searching the user
     * @param roleDN DN used when searching the role DN. Override the baseDN if
     *        it is defined
     * @param roleSearchFilter Filter used when searching the role
     * @param algorithm Default algorithm. If specified, the default is not
     *        'clear' password
     * @throws Exception if the resource can't be added
     */
    public void addJResourceLDAP(String name, String initialContextFactory, String providerUrl,
            String securityAuthentication, String securityPrincipal, String securityCredentials,
            String securityProtocol, String language, String referral, String stateFactories,
            String authenticationMode, String userPasswordAttribute, String userRolesAttribute,
            String roleNameAttribute, String baseDN, String userDN, String userSearchFilter, String roleDN,
            String roleSearchFilter, String algorithm) throws Exception {

        // Build a new JResourceLDAP
        JResourceLDAP jResourceLDAP = new JResourceLDAP();
        jResourceLDAP.setName(name);
        jResourceLDAP.setInitialContextFactory(initialContextFactory);
        jResourceLDAP.setProviderUrl(providerUrl);
        jResourceLDAP.setSecurityAuthentication(securityAuthentication);
        jResourceLDAP.setSecurityPrincipal(securityPrincipal);
        jResourceLDAP.setSecurityCredentials(securityCredentials);
        jResourceLDAP.setSecurityProtocol(securityProtocol);
        jResourceLDAP.setLanguage(language);
        jResourceLDAP.setReferral(referral);
        jResourceLDAP.setStateFactories(stateFactories);
        jResourceLDAP.setAuthenticationMode(authenticationMode);
        jResourceLDAP.setUserPasswordAttribute(userPasswordAttribute);
        jResourceLDAP.setUserRolesAttribute(userRolesAttribute);
        jResourceLDAP.setRoleNameAttribute(roleNameAttribute);
        jResourceLDAP.setBaseDN(baseDN);
        jResourceLDAP.setUserDN(userDN);
        jResourceLDAP.setUserSearchFilter(userSearchFilter);
        jResourceLDAP.setRoleDN(roleDN);
        jResourceLDAP.setRoleSearchFilter(roleSearchFilter);
        jResourceLDAP.setAlgorithm(algorithm);

        // Build xml
        StringBuffer xml = new StringBuffer(JResources.HEADER_XML);
        xml.append("<jonas-realm>");
        xml.append("<jonas-ldaprealm>");
        xml.append(jResourceLDAP.toXML());
        xml.append("</jonas-ldaprealm>");
        xml.append("</jonas-realm>");

        // Add the resource
        addResources(xml.toString());

    }

    /**
     * Bind the given resource with the given name and register with a new
     * MBean.
     * @param name resource name
     * @param jResource resource
     */
    public void bindResource(String name, JResource jResource) {
        // bind the resource into the jndi
        if (bindResourcesIntoJndi) {
            try {
                ictx.rebind(jResource.getName(), jResource);
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "jResource " + jResource.getName() + " bound into the registry.");
                }
            } catch (NamingException e) {
                logger.log(BasicLevel.ERROR, "Cannot bind the resource '" + jResource.getName() + "' into JNDI", e);
            }
        }

        try {
            // register security factory mbean
            if (jResource instanceof JResourceMemory) {
                mbeanServer.registerMBean(jResource, JonasObjectName.securityMemoryFactory(jResource.getName()));
            } else if (jResource instanceof JResourceDS) {
                mbeanServer.registerMBean(jResource, JonasObjectName.securityDatasourceFactory(jResource.getName()));
            } else if (jResource instanceof JResourceLDAP) {
                mbeanServer.registerMBean(jResource, JonasObjectName.securityLdapFactory(jResource.getName()));
            }
        } catch (ServiceException se) {
            logger.log(BasicLevel.ERROR, "JMX service not available", se);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Can not register the MBean for the resource " + jResource.getName() + " : "
                    + e.getMessage());
            throw new ServiceException("Can not register the MBean for the resource " + jResource.getName() + " : "
                    + e.getMessage());
        }

    }
}
