/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2007 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
 *
 * Initial developer(s): Florent BENOIT & Ludovic BERT
 * --------------------------------------------------------------------------
 * $Id: JResourceDS.java 18852 2009-10-09 14:31:22Z danesa $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.security.internal.realm.factory;

import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.sql.DataSource;

import org.ow2.jonas.security.internal.realm.lib.HashHelper;
import org.ow2.jonas.security.internal.realm.principal.User;
import org.ow2.jonas.security.realm.factory.JResourceException;
import org.ow2.jonas.security.realm.principal.JUser;

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

/**
 * This class extends the JResource class for the Datasource implementation.
 * @author Florent Benoit
 */
public class JResourceDS extends AbstractJResource {

    /**
     * Type of the factory
     */
    private static final String FACTORY_TYPE = "org.ow2.jonas.security.realm.factory.JResourceDS";

    /**
     * Name of the factory
     */
    private static final String FACTORY_NAME = "org.ow2.jonas.security.realm.factory.JResourceDSFactory";

    /**
     * Name of the datasource resource to use.
     */
    private String dsName = null;

    /**
     * Name of table which have the username/password
     */
    private String userTable = null;

    /**
     * Column of the username of the user table
     */
    private String userTableUsernameCol = null;

    /**
     * Column of the password of the user table
     */
    private String userTablePasswordCol = null;

    /**
     * Name of table which have the username/role
     */
    private String roleTable = null;

    /**
     * Column of the username of the role table
     */
    private String roleTableUsernameCol = null;

    /**
     * Column of the role of the role table
     */
    private String roleTableRolenameCol = null;

    /**
     * Default algorithm. If specified, the default is not 'clear' password
     */
    private String algorithm = null;

    /**
     * Datasource to use
     */
    private DataSource dataSource = null;

    /**
     * User defined query for retrieving principals
     */
    private String userPrincipalsQuery = null;

    /**
     * User defined query for retrieving roles
     */
    private String userRolesQuery = null;

    /**
     * Constructor. Use the super constructor
     * @throws Exception if super constructor fail
     */
    public JResourceDS() throws Exception {
        super();

    }

    /**
     * Set the name of the resource to use
     * @param dsName name of the resource
     */
    public void setDsName(String dsName) {
        this.dsName = dsName;
    }

    /**
     * Set the name of the table which have the username/password
     * @param userTable name of the table which have the username/password
     */
    public void setUserTable(String userTable) {
        this.userTable = userTable;
    }

    /**
     * Set the name of the column of the username of the user table
     * @param userTableUsernameCol name of the column of the username of the
     *        user table
     */
    public void setUserTableUsernameCol(String userTableUsernameCol) {
        this.userTableUsernameCol = userTableUsernameCol;
    }

    /**
     * Set the name of column of the password of the user table
     * @param userTablePasswordCol name of column of the password of the user
     *        table
     */
    public void setUserTablePasswordCol(String userTablePasswordCol) {
        this.userTablePasswordCol = userTablePasswordCol;
    }

    /**
     * Set the name of table which have the username/role
     * @param roleTable name of table which have the username/role
     */
    public void setRoleTable(String roleTable) {
        this.roleTable = roleTable;
    }

    /**
     * Set the name of the column of the username of the role table
     * @param roleTableUsernameCol name of the column of the username of the
     *        role table
     */
    public void setRoleTableUsernameCol(String roleTableUsernameCol) {
        this.roleTableUsernameCol = roleTableUsernameCol;
    }

    /**
     * Set the name of the column of the role of the role table
     * @param roleTableRolenameCol name of the column of the role of the role
     *        table
     */
    public void setRoleTableRolenameCol(String roleTableRolenameCol) {
        this.roleTableRolenameCol = roleTableRolenameCol;
    }

    /**
     * Set the default algorithm to use
     * @param algorithm algorithm to be used
     */
    public void setAlgorithm(String algorithm) {
        this.algorithm = algorithm;
    }

    /**
     * Set the user defined query for retrieving principals
     * @param userPrincipalsQuery the user defined query for retrieving
     *        principals
     */
    public void setUserPrincipalsQuery(String userPrincipalsQuery) {
        this.userPrincipalsQuery = userPrincipalsQuery;
    }

    /**
     * Set the user defined query for retrieving roles
     * @param userRolesQuery the user defined query for retrieving roles
     */
    public void setUserRolesQuery(String userRolesQuery) {
        this.userRolesQuery = userRolesQuery;
    }

    /**
     * Get the name of the resource to use
     * @return name of the resource
     */
    public String getDsName() {
        return dsName;
    }

    /**
     * Get the name of the table which have the username/password
     * @return name of the table which have the username/password
     */
    public String getUserTable() {
        return userTable;
    }

    /**
     * Get the name of the column of the username of the user table
     * @return name of the column of the username of the user table
     */
    public String getUserTableUsernameCol() {
        return userTableUsernameCol;
    }

    /**
     * Get the name of column of the password of the user table
     * @return name of column of the password of the user table
     */
    public String getUserTablePasswordCol() {
        return userTablePasswordCol;
    }

    /**
     * Get the name of table which have the username/role
     * @return name of table which have the username/role
     */
    public String getRoleTable() {
        return roleTable;
    }

    /**
     * Get the name of the column of the username of the role table
     * @return name of the column of the username of the role table
     */
    public String getRoleTableUsernameCol() {
        return roleTableUsernameCol;
    }

    /**
     * Get the name of the column of the role of the role table
     * @return name of the column of the role of the role table
     */
    public String getRoleTableRolenameCol() {
        return roleTableRolenameCol;
    }

    /**
     * Get the default algorithm
     * @return the default algorithm
     */
    public String getAlgorithm() {
        return algorithm;
    }

    /**
     * Gets the user defined query for retrieving principals
     * @return the user defined query for retrieving principals
     */
    public String setUserPrincipalsQuery() {
        return userPrincipalsQuery;
    }

    /**
     * Gets the user defined query for retrieving roles
     * @return the user defined query for retrieving roles
     */
    public String setUserRolesQuery() {
        return userRolesQuery;
    }

    /**
     * Check if a user is found and return it
     * @param username the wanted user name
     * @return the user found or null
     * @throws JResourceException if there is a SQLException
     */
    public JUser findUser(String username) throws JResourceException {

        if (username == null) {
            return null;
        }

        // Build new user
        User user = new User();

        Connection connection = getConnection();

        user.setName(username);

        // Get the password of the user
        ResultSet rs = null;
        String password = null;
        try {
            PreparedStatement usrStmt = userStatement(connection, username);
            rs = usrStmt.executeQuery();
            if (rs == null || !rs.next()) {
                if (rs != null) {
                    rs.close();
                }
                usrStmt.close();
                closeConnection(connection);
                throw new JResourceException("No user found with username '" + username + "'.");
            }

            int records = rs.getRow();
            if (records > 1) {
                getLogger().log(BasicLevel.ERROR, "There are more than one user with the name" + username);
            }
            password = rs.getString(1).trim();
            rs.close();
            usrStmt.close();
        } catch (SQLException sqle) {
            closeConnection(connection);
            throw new JResourceException(sqle.getMessage(), sqle);
        }

        if (password == null) {
            closeConnection(connection);
            return null;
        }
        user.setPassword(password);

        // Get the roles of the user
        try {
            PreparedStatement rlStmt = roleStatement(connection, username);
            rs = rlStmt.executeQuery();

            while (rs.next()) {
                String role = rs.getString(1).trim();
                user.addRole(role);
            }
            rs.close();
            rlStmt.close();
        } catch (SQLException sqle) {
            closeConnection(connection);
            throw new JResourceException(sqle.getMessage(), sqle);
        }

        // Commit the connection if it is not automatic
        try {
            if (!connection.getAutoCommit()) {
                connection.commit();
            }
        } catch (SQLException sqle) {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(BasicLevel.DEBUG, "Cannot commit on the current connection :  : '"
                        + sqle.getMessage() + "'");
            }
        }
        closeConnection(connection);
        return user;
    }

    /**
     * Check if the given credential is the right credential for the given user
     * @param user user to check its credentials
     * @param credentials the given credentials
     * @return true if the credential is valid for this user
     */
    public boolean isValidUser(JUser user, String credentials) {

        boolean validated = false;

        //Get algorithm and hashpassword
        String pass = user.getHashPassword().getPassword();
        String algo = user.getHashPassword().getAlgorithm();

        // Crypt password ?
        if (algo != null) {
            try {
                validated = HashHelper.hashPassword(credentials, algo)
                        .equalsIgnoreCase(pass);
            } catch (NoSuchAlgorithmException nsae) {
                getLogger().log(
                        BasicLevel.ERROR,
                        "Can't make a password with the algorithm " + algo
                                + ". " + nsae.getMessage());
            }
        } else if ((algorithm != null) && (!algorithm.equals(""))) {
            // Encode password with the specified algorithm (no clear)
            try {
                validated = HashHelper.hashPassword(credentials, algorithm)
                        .equalsIgnoreCase(pass);
            } catch (NoSuchAlgorithmException nsae) {
                getLogger().log(
                        BasicLevel.ERROR,
                        "Can't make a password with the algorithm " + algorithm
                                + ". " + nsae.getMessage());
            }
        } else {
            // clear password
            validated = credentials.equals(pass);
        }
        return validated;
    }

    /**
     * Get all the roles (from the roles and from the groups) of the given user
     * @param user the given user
     * @return the array list of all the roles for a given user
     * @throws JResourceException if it fails
     */
    public ArrayList getArrayListCombinedRoles(JUser user)
            throws JResourceException {

        ArrayList allCombinedRoles = new ArrayList();

        // Return empty array if user null
        if (user == null) {
            return allCombinedRoles;
        }
        // Add all user roles
        String[] userRoles = user.getArrayRoles();
        for (int r = 0; r < userRoles.length; r++) {
            String roleName = userRoles[r];
            if (!allCombinedRoles.contains(roleName)) {
                allCombinedRoles.add(roleName);
            }
        }
        user.setCombinedRoles(allCombinedRoles);

        return allCombinedRoles;
    }

    /**
     * String representation of the MemoryRealm
     * @return the xml representation of the MemoryRealm
     */
    public String toXML() {
        StringBuffer xml = new StringBuffer("    <dsrealm name=\"");
        xml.append(getName());
        xml.append("\"\n             dsName=\"");
        if (dsName != null) {
            xml.append(dsName);
        }
        xml.append("\"\n             userTable=\"");
        if (userTable != null) {
            xml.append(userTable);
        }
        xml.append("\" userTableUsernameCol=\"");
        if (userTableUsernameCol != null) {
            xml.append(userTableUsernameCol);
        }
        xml.append("\" userTablePasswordCol=\"");
        if (userTablePasswordCol != null) {
            xml.append(userTablePasswordCol);
        }
        xml.append("\"\n             roleTable=\"");
        if (roleTable != null) {
            xml.append(roleTable);
        }
        xml.append("\" roleTableUsernameCol=\"");
        if (roleTableUsernameCol != null) {
            xml.append(roleTableUsernameCol);
        }
        xml.append("\" roleTableRolenameCol=\"");
        if (roleTableRolenameCol != null) {
            xml.append(roleTableRolenameCol);
        }

        if ((userPrincipalsQuery != null) && (!userPrincipalsQuery.equals(""))) {
            xml.append("\"\n             userPrincipalsQuery=\"");
            xml.append(userPrincipalsQuery);
        }

        if ((userRolesQuery != null) && (!userRolesQuery.equals(""))) {
            xml.append("\"\n             userRolesQuery=\"");
            xml.append(userRolesQuery);
        }

        if ((algorithm != null) && (!algorithm.equals(""))) {
            xml.append("\"\n             algorithm=\"");
            xml.append(algorithm);
        }

        xml.append("\" />");
        return xml.toString();
    }

    /**
     * The string representation of this realm is the XML
     * @return XML representation
     */
    public String toString() {
        return this.toXML();
    }

    /**
     * Retrieves the Reference of the object. The Reference contains the factory
     * used to create this object and the optional parameters used to configure
     * the factory.
     * @return the non-null Reference of the object.
     * @throws NamingException if a naming exception was encountered while
     *         retrieving the reference.
     */
    public Reference getReference() throws NamingException {

        // Build the reference to the factory FACTORY_TYPE
        Reference reference = new Reference(FACTORY_TYPE, FACTORY_NAME, null);

        // Add ref addr
        reference.add(new StringRefAddr("name", getName()));
        reference.add(new StringRefAddr("dsName", dsName));
        reference.add(new StringRefAddr("userTable", userTable));
        reference.add(new StringRefAddr("userTableUsernameCol",
                userTableUsernameCol));
        reference.add(new StringRefAddr("userTablePasswordCol",
                userTablePasswordCol));
        reference.add(new StringRefAddr("roleTable", roleTable));
        reference.add(new StringRefAddr("roleTableUsernameCol",
                roleTableUsernameCol));
        reference.add(new StringRefAddr("roleTableRolenameCol",
                roleTableRolenameCol));
        reference.add(new StringRefAddr("userPrincipalsQuery",
                userPrincipalsQuery));
        reference.add(new StringRefAddr("userRolesQuery", userRolesQuery));
        reference.add(new StringRefAddr("algorithm", algorithm));

        return reference;
    }

    /**
     * Try to close the given connection
     * @param c the connection to close
     */
    private void closeConnection(Connection c) {
        if (c == null) {
            return;
        }
        try {
            c.close();
        } catch (Exception e) {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(BasicLevel.DEBUG, "Can not close the connection");
            }
        }

    }

    /**
     * Get a connection from the dataSource
     * @return the connection from the dataSource
     * @throws JResourceException if an SQLException is thrown by
     *         dataSource.getConnection()
     */
    private Connection getConnection() throws JResourceException {

        // If no dataSource, get an instance
        if (dataSource == null) {
            // Finds DataSource from JNDI
            Context initialContext = null;
            try {
                initialContext = new InitialContext();
                dataSource = (DataSource) initialContext.lookup(dsName);
            } catch (Exception e) {
                String err = "Cannot find resource " + dsName
                        + " in the registry " + e.getMessage();
                getLogger().log(BasicLevel.ERROR, err);
                throw new JResourceException(err, e);
            }
        }

        // Retrieve connection from the datasource
        Connection c = null;
        try {
            c = dataSource.getConnection();
        } catch (SQLException sqle) {
            getLogger().log(BasicLevel.ERROR, sqle.getMessage());
            throw new JResourceException(sqle.getMessage(), sqle);
        }

        return c;
    }

    /**
     * Return the user query. It select the password for a specific user
     * @return the user query
     */
    private String userQuery() {

        if (userPrincipalsQuery != null) {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(
                        BasicLevel.DEBUG,
                        "Return user defined SQL query for user"
                                + userPrincipalsQuery);
            }
            return userPrincipalsQuery;
        } else {

            /*
             * SELECT userTablePasswordCol FROM userTable WHERE
             * userTableUsernameCol = ?
             */
            StringBuffer stringBuffer = new StringBuffer("SELECT ");
            stringBuffer.append(userTablePasswordCol);
            stringBuffer.append(" FROM ");
            stringBuffer.append(userTable);
            stringBuffer.append(" WHERE ");
            stringBuffer.append(userTableUsernameCol);
            stringBuffer.append(" = ?");
            return (stringBuffer.toString());
        }
    }

    /**
     * Return the roles query. It select the roles for a specific user
     * @return the roles query
     */
    private String rolesOfUserQuery() {

        if (userRolesQuery != null) {
            if (getLogger().isLoggable(BasicLevel.DEBUG)) {
                getLogger().log(
                        BasicLevel.DEBUG,
                        "Return user defined SQL query for roles"
                                + userRolesQuery);
            }
            return userRolesQuery;
        } else {

            /*
             * SELECT r.roleTableRolenameCol FROM userTable u, roleTable r WHERE
             * u.userTableUsernameCol = r.roleTableUsernameCol AND
             * u.userTableUsernameCol = ?
             */

            StringBuffer stringBuffer = new StringBuffer("SELECT r.");
            stringBuffer.append(roleTableRolenameCol);
            stringBuffer.append(" FROM ");
            stringBuffer.append(userTable);
            stringBuffer.append(" u, ");
            stringBuffer.append(roleTable);
            stringBuffer.append(" r WHERE u.");
            stringBuffer.append(userTableUsernameCol);
            stringBuffer.append(" = r.");
            stringBuffer.append(roleTableUsernameCol);
            stringBuffer.append(" AND u.");
            stringBuffer.append(userTableUsernameCol);
            stringBuffer.append(" = ?");
            return stringBuffer.toString();
        }
    }

    /**
     * Return a statement for the given username by using the userQuery query
     * @param connection connection to use
     * @param username the given user
     * @return a statement for the given user
     * @throws SQLException if the SQL statement fails
     */
    private PreparedStatement userStatement(Connection connection,
            String username) throws SQLException {

        if (getLogger().isLoggable(BasicLevel.DEBUG)) {
            getLogger().log(BasicLevel.DEBUG,
                    "Creating user statement for the user '" + username + "'");
        }

        PreparedStatement userStatement = connection.prepareStatement(userQuery());

        userStatement.setString(1, username);
        return userStatement;
    }

    /**
     * Return a statement for the given username by using the rolesOfUserQuery
     * query
     * @param connection connection to use
     * @param username the given user
     * @return a roles of user statement for the given user
     * @throws SQLException if the SQL statement fails
     */
    private PreparedStatement roleStatement(Connection connection,
            String username) throws SQLException {

        if (getLogger().isLoggable(BasicLevel.DEBUG)) {
            getLogger().log(BasicLevel.DEBUG,
                    "Creating role statement for the user " + username + "'");
        }
        PreparedStatement roleStatement = connection.prepareStatement(rolesOfUserQuery());

        roleStatement.setString(1, username);

        return roleStatement;
    }

    /**
     * Remove all the Mbeans used by this resource
     * @throws JResourceException if the MBeans can not be removed
     */
    public void removeMBeans() throws JResourceException {
        //no MBeans
    }

}
