/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2009-2011 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.antmodular.jonasbase.security;

import org.apache.tools.ant.BuildException;
import org.ow2.jonas.antmodular.jonasbase.bootstrap.AbstractJOnASBaseAntTask;
import org.ow2.jonas.antmodular.jonasbase.bootstrap.JReplace;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Allow to configure JAAS
 * @author Julien Legrand
 * @author Jeremy Cazaux (support to <user/> and <admin/> element
 */
public class Jaas extends AbstractJOnASBaseAntTask {

    /**
     * Info for the logger.
     */
    private static final String INFO = "[JAAS] ";

    /**
     * Token for the begin of the jaas.config configuration file.
     */
    private static final String TOKEN_BEGIN_CONF_FILE = "jaasclient {";

    /**
     * Name of JAAS configuration file.
     */
    public static final String JAAS_CONF_FILE = "jaas.config";

    /**
     * Class name of the JResourceLoginModule
     */
    public static final String DEFAULT_LOGIN_MODULE_NAME = "org.ow2.jonas.security.auth.spi.JResourceLoginModule";

    /**
     * Token for the JMX authentification method
     */
    public static final String JMX_AUTHENTIFICATION_METHOD_TOKEN = "jonas.service.jmx.authentication.method";

    /**
     * Value for the JMX authentification method
     */
    public static final String JMX_AUTHENTIFICATION_METHOD_VALUE = "jmx.remote.x.login.config";
    
    /**
     * Token for the JMX authentification parameter
     */
    public static final String JMX_AUTHENTIFICATION_PARAMETER_TOKEN = "jonas.service.jmx.authentication.parameter";

    /**
     * Value for the JMX authentification parameter
     */
    public static final String JMX_AUTHENTIFICATION_PARAMETER_VALUE = "jaas-jmx";

    /**
     * Token for the JMX security activation
     */
    public static final String JMX_IS_SECURED_TOKEN = "jonas.service.jmx.secured";

    /**
     * Value for the JMX security activation
     */
    public static final String JMX_IS_SECURED_VALUE = "true";

    /**
     * JOnAS Admin username token
     */
    public static final String JONAS_ADMIN_USERNAME_TOKEN = "jonas.adminClient.username";

    /**
     * JOnAS Admin password token
     */
    public static final String JONAS_ADMIN_PASSWORD_TOKEN = "jonas.adminClient.password";

    /**
     * Name of the realm conf file
     */
    public static final String JONAS_REALM_CONF_FILE = "jonas-realm.xml";

    /**
     * List of {@link User} to configure
     */
    private List<User> users;

    /**
     * List of {@link User} to configure
     */
    private List<User> admins;

    /**
     * List of users which are defined in the tempalte file
     */
    private List<User> templateUsers;

    /**
     * List of roles which are defined in the template file
     */
    private Map<String, String> templateRoles;

    /**
     * Whether JMX security should be enabled
     */
    private boolean secureJmx = false;

    /**
     * EOL
     */
    public static String EOL = "\n";

    /**
     * Separator use in the realme file
     */
    public static String REALME_SEPARATOR = "      ";

    /**
     * Realm's Users token
     */
    public static String USERS_TOKEN = "</users>";

    /**
     * Realm's role token
     */
    public static String ROLES_TOKEN = "</roles>";

    /**
     * Constructor
     */
    public Jaas(){
        super();
        this.users = new ArrayList<User>();
        this.admins = new ArrayList<User>();
        this.templateUsers = new ArrayList<User>();
        this.templateRoles = new HashMap<String, String>();
    }

    /**
     * Configure a JAAS entry
     * @param entry the entry to configure.
     */
    public void addConfiguredJaasEntry(final JaasEntry entry){

        JReplace propertyReplace = new JReplace();
        propertyReplace.setConfigurationFile(JAAS_CONF_FILE);
        propertyReplace.setToken(TOKEN_BEGIN_CONF_FILE);
        StringBuffer value = new StringBuffer();

        value.append(entry.getName() + "{" + "\n");
        for(LoginModule lm : entry.getLoginModules()){

            if(lm instanceof JResourceLoginModule){

                JResourceLoginModule rlm = (JResourceLoginModule) lm;

                value.append("    " + DEFAULT_LOGIN_MODULE_NAME + " " + lm.getFlag() + "\n");
                value.append("    " + "resourceName=\"" + rlm.getResourceName() + "\"" + "\n");

                if(rlm.getServerName() != null && !"".equals(rlm.getServerName())){
                    value.append("    " + "serverName=\"" + rlm.getServerName() + "\"" + "\n");
                }
                if(rlm.isUseUpperCaseUsername()){
                    value.append("    " + "useUpperCaseUsername=\"" + rlm.isUseUpperCaseUsername() + "\"" + "\n");
                }
                if(rlm.isCertCallback()){
                    value.append("    " + "certCallback=\"" + rlm.isCertCallback() + "\"" + "\n");
                }
            }
            
            value.append("\n");
        }

        value.append("    " + ";" + "\n");
        value.append("};" + "\n");
        value.append("\n");
        value.append(TOKEN_BEGIN_CONF_FILE);

        propertyReplace.setValue(value.toString());
        propertyReplace.setLogInfo(INFO + "Add the JAAS entry : " + entry.getName());
        addTask(propertyReplace);
    }

    /**
     * @param admin The admin to configure
     */
    public void addConfiguredAdmin(final User admin) {
        if (admin.getName() == null) {
            throw new BuildException("Admin username is not set.");            
        }
        if (admin.getPassword() == null) {
            throw new BuildException("Admin password is not set");
        }

        this.admins.add(admin);
    }

    /**
     * @param user The user to configure
     */
    public void addConfiguredUser(final User user) {
        if (user.getName() == null) {
            throw new BuildException("Username is not set.");
        }
        if (user.getPassword() == null) {
            throw new BuildException("User's password is not set");
        }

        this.users.add(user);
    }

    /**
     * Enable or disable JMX security.
     *
     * @param enabled
     *            (true or false)
     *            the default value is currently false in jonas.properties
     */
    public void setSecureJmx(final boolean enabled) {
        this.secureJmx = enabled;
    }

    /**
    * Execute this task.
    */
    public void execute() {
        super.execute();

        final String confDirPath = getJOnASBase() + CONF_DIR;      

        if (secureJmx) {
            if (this.admins.isEmpty()) {
                throw new BuildException("JMX security is enabled, but with no admin user defined. " +
                    "Please correct your configuration and run again.");
            }

            //configure JMX security 
            super.createServiceNameReplace(JMX_IS_SECURED_VALUE, INFO, confDirPath, JMX_IS_SECURED_TOKEN);            
            
            super.createServiceNameReplace(JMX_AUTHENTIFICATION_METHOD_VALUE, INFO, confDirPath,
                    JMX_AUTHENTIFICATION_METHOD_TOKEN);

            super.createServiceNameReplace(JMX_AUTHENTIFICATION_PARAMETER_VALUE, INFO, confDirPath,
                    JMX_AUTHENTIFICATION_PARAMETER_TOKEN);
        }

        if (this.admins.size() > 0) {
            //update jonasAdmin template
            super.createServiceNameReplace(this.admins.get(0).getName(), INFO, confDirPath, JONAS_ADMIN_USERNAME_TOKEN);
            super.createServiceNameReplace(this.admins.get(0).getPassword(), INFO, confDirPath, JONAS_ADMIN_PASSWORD_TOKEN);
        }

        initUsers(new File(confDirPath, JONAS_REALM_CONF_FILE));
        
        //update jonas-realm with the list of users
        for (User user: this.users) {
            String username = user.getName();
            String password = user.getPassword();
            String roles = user.getRoles();
            String description = user.getDescription();

            User templateUser = isUserAlreadyDefined(user);
            if (templateUser != null) {
                
                String templateUserName = templateUser.getName();
                String templateUserPassword = templateUser.getPassword();
                String templateUserRoles = templateUser.getRoles();
                String templateUserGroups = templateUser.getGroups();
                
                //if the user's name is already set and if the password has changed, update of the password
                if (!templateUserPassword.equals(user.getPassword())) {

                    //if the user to update is an admin (already defined), throw an exception
                    if ((templateUserGroups != null && templateUserGroups.contains("jonas")) ||
                        (templateUserRoles != null && templateUserRoles.contains("jonas-admin"))) {
                        throw new BuildException("Cannot update the password and/or role of the username '" +
                                templateUserName + "'. An admin is already defined with this name.");
                    }

                    //update of the user
                    String userToken = "<user name=\"" + templateUserName + "\" password=\"" +
                            templateUserPassword + "\" roles=\"" + templateUserRoles + "\" />";
                    String userValue = "<user name=\"" + username + "\" password=\"" + password + "\" roles=\"" + roles
                            + "\" />";
                    JReplace propertyReplace = new JReplace();
                    propertyReplace.setConfigurationFile(JONAS_REALM_CONF_FILE);
                    propertyReplace.setToken(userToken);
                    propertyReplace.setValue(userValue);
                    propertyReplace.setDestDir(this.destDir);
                    propertyReplace.setLogInfo(INFO + "Setting password of '" + username + "' to '" + password + "'");
                    propertyReplace.execute();                    
                } 
            } else {
                //create a new user
                String userValue = "  <user name=\"" + username + "\" password=\"" + password + "\" roles=\"" + roles
                        + "\" />" + EOL + REALME_SEPARATOR + USERS_TOKEN; 
                JReplace propertyReplace = new JReplace();
                propertyReplace.setConfigurationFile(JONAS_REALM_CONF_FILE);
                propertyReplace.setToken(USERS_TOKEN);
                propertyReplace.setValue(userValue);
                propertyReplace.setDestDir(this.destDir);
                propertyReplace.setLogInfo(INFO + "Creating a new user with name equals to '" + username +
                        "'  ,password equals to '" + password + "' and role equals to '" + roles + "'");
                propertyReplace.execute(); 
            }

            //check if the role is already defined
            if (!this.templateRoles.containsKey(roles)) {
                //create a new role
                StringBuilder roleValue = new StringBuilder();
                roleValue.append("  <role name=\"" + roles +  "\" ");
                if (description != null) {
                    roleValue.append("description=\"" + description + "\" ");
                }
                roleValue.append("/>" + EOL + REALME_SEPARATOR + ROLES_TOKEN);
                JReplace propertyReplace = new JReplace();
                propertyReplace.setConfigurationFile(JONAS_REALM_CONF_FILE);
                propertyReplace.setToken(ROLES_TOKEN);
                propertyReplace.setValue(roleValue.toString());
                propertyReplace.setDestDir(this.destDir);
                propertyReplace.setLogInfo(INFO + "Creating a new role with name equals to '" + roles + "'  and " +
                        "description equals to '" + description + "'");
                propertyReplace.execute();
            } else {
                //update the role if the description has been updated
                String templateDescription = this.templateRoles.get(roles);
                if (description != null && !templateDescription.equals(description)) {
                    String roleToken = "<role name=\"" + roles + "\" description=\"" + templateDescription  + "\" />";
                    StringBuilder roleValue = new StringBuilder();
                    roleValue.append("<role name=\"" + roles + "\" ");
                    if (description != null) {
                        roleValue.append("description=\"" + description  + "\" ");
                    }
                    roleValue.append("/>");

                    JReplace propertyReplace = new JReplace();
                    propertyReplace.setConfigurationFile(JONAS_REALM_CONF_FILE);
                    propertyReplace.setToken(roleToken);
                    propertyReplace.setValue(roleValue.toString());
                    propertyReplace.setDestDir(this.destDir);
                    propertyReplace.setLogInfo(INFO + "Setting the description of role " + roles + "  to : " + description);
                    propertyReplace.execute();
                } 
            }
            
        }

        //update jonas-realm with the list of admins
        for (User admin: this.admins) {
            String username = admin.getName();
            String password = admin.getPassword();

            User userAdmin = new User();
            userAdmin.setName(username);
            userAdmin.setPassword(password);
            User templateAdmin = isUserAlreadyDefined(userAdmin);

            if (templateAdmin != null) {
                if (!templateAdmin.getPassword().equals(userAdmin.getPassword())) {
                    //if the user's name is already set and if the password has changed, update of the password
                    String userToken = null;
                    if (templateAdmin.getRoles() != null) {
                        userToken = "<user name=\"" + templateAdmin.getName() + "\" password=\"" +
                                templateAdmin.getPassword() + "\" roles=\"" + templateAdmin.getRoles() + "\" />";
                    } else if (templateAdmin.getGroups() != null) {
                        userToken = "<user name=\"" + templateAdmin.getName() + "\" password=\"" +
                                templateAdmin.getPassword() + "\" groups=\"" + templateAdmin.getGroups() + "\" />";
                    } else {
                        throw new BuildException(JONAS_REALM_CONF_FILE + " template configuration file is incorrect. " +
                                "No 'groups' or 'roles' attribute for the user " + templateAdmin.getName());
                    }

                    String userValue = "<user name=\"" + username + "\" password=\"" + password + "\" groups=\"jonas\"/>";
                    JReplace propertyReplace = new JReplace();
                    propertyReplace.setConfigurationFile(JONAS_REALM_CONF_FILE);
                    propertyReplace.setToken(userToken);
                    propertyReplace.setValue(userValue);
                    propertyReplace.setDestDir(this.destDir);
                    propertyReplace.setLogInfo(INFO + "Setting password of '" + username + "' to '" + password + "'");
                    propertyReplace.execute();
                }
            } else {
                //create the admin
                String userValue = "  <user name=\"" + username + "\" password=\"" + password + "\" groups=\"jonas\"/>"
                        + EOL + REALME_SEPARATOR + USERS_TOKEN;
                JReplace propertyReplace = new JReplace();
                propertyReplace.setConfigurationFile(JONAS_REALM_CONF_FILE);
                propertyReplace.setToken(USERS_TOKEN);
                propertyReplace.setValue(userValue);
                propertyReplace.setDestDir(this.destDir);
                propertyReplace.setLogInfo(INFO + "Creating a new admin user with name equals to '" + username +
                        "'  and password equals to '" + password + "'");
                propertyReplace.execute();
            }
        }

        executeAllTask();
    }

    /**
     *
     * @param realmConfFile The realm configuration file
     */
    private void initUsers(final File realmConfFile) {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        DocumentBuilder db;
        Document document = null;

        try {
            db = dbf.newDocumentBuilder();
        } catch (ParserConfigurationException e) {
            throw new BuildException("Cannot instanciate a new DocumentBuilder", e);
        }

        // Dummy entity resolvers
        db.setEntityResolver(new EmptyEntityResolver());

        try {
            document = db.parse(realmConfFile);
        } catch (SAXException e) {
            throw new BuildException("Cannot parse the resource " + realmConfFile, e);
        } catch (IOException e) {
            throw new BuildException("Cannot parse the resource " + realmConfFile, e);
        }

        //get the list of defaults (or template) users
        NodeList nodeList = document.getElementsByTagName("user");
        for (int i = 0; i < nodeList.getLength(); i++) {
            Element elt = (Element) nodeList.item(i);
            User user = new User();
            String name = elt.getAttribute("name");
            if (name != null && !"".equals(name)) {
                user.setName(name);                
            }
            String password = elt.getAttribute("password"); 
            if (password != null && !"".equals(password)) {
                user.setPassword(password);    
            }
            String roles = elt.getAttribute("roles");
            if (roles != null && !"".equals(roles)) {
                user.setRoles(roles);
            }
            String groups = elt.getAttribute("groups");
            if (groups != null && !"".equals(groups)) {
                user.setGroups(groups);
            }
            this.templateUsers.add(user);
        }

        //get the list of default (or template) roles
        nodeList = document.getElementsByTagName("role");
        for (int i = 0; i < nodeList.getLength(); i++) {
            Element elt = (Element) nodeList.item(i);
            this.templateRoles.put(elt.getAttribute("name"), elt.getAttribute("description"));
        }
    }

    /**
     * @param user A User
     * @return the template user if the user is already defined. Otherwise, null.
     */
    private User isUserAlreadyDefined(final User user) {
        for(User templateUser: this.templateUsers) {
            if (templateUser.getName().equals(user.getName())) {
                return templateUser;
            }
        }
        return null;
    }
    
}
