/**
 *  '$RCSfile$'
 *  Copyright: 2013 Regents of the University of California and the
 *             National Center for Ecological Analysis and Synthesis
 *
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package edu.ucsb.nceas.metacat.authentication;

import java.io.Console;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.apache.commons.configuration.tree.xpath.XPathExpressionEngine;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import edu.ucsb.nceas.metacat.AuthInterface;
import edu.ucsb.nceas.metacat.AuthLdap;
import edu.ucsb.nceas.metacat.properties.PropertyService;
import edu.ucsb.nceas.metacat.util.SystemUtil;
import edu.ucsb.nceas.utilities.PropertyNotFoundException;

/**
 * This an authentication class base on a username/password file.
 * It is an alternative authentication mechanism of the ldap authentication.
 * The password file looks like:
 *<?xml version="1.0" encoding="UTF-8" ?>
 * <subjects>
 *  <users>
 *      <user dn="uid=tao,o=NCEAS,dc=ecoinformatics,dc=org">
 *          <password>*******</password>
 *          <email>foo@foo.com</email>
 *          <surName>Smith</surName>
 *          <givenName>John</givenName>
 *          <organization>NCEAS</organization>
 *          <memberof>nceas-dev</memberof>
 *      </user>
 *  </users>
 *  <groups>
 *    <group name="nceas-dev">
 *        <description>developers at NCEAS</description>
 *    </group>
 *  </groups>
 * </subjects>
 * http://commons.apache.org/proper/commons-configuration/userguide/howto_xml.html
 * @author tao
 *
 */
public class AuthFile implements AuthInterface {
    private static final String ORGANIZATIONNAME = "Unknown";
    private static final String ORGANIZATION = "organization";
    private static final String NAME = "name";
    private static final String DN = "dn";
    private static final String DESCRIPTION = "description";
    private static final String PASSWORD = "password";
    private static final String SLASH = "/";
    private static final String AT = "@";
    private static final String SUBJECTS = "subjects";
    private static final String USERS = "users";
    private static final String USER = "user";
    private static final String GROUPS = "groups";
    private static final String GROUP = "group";
    private static final String EMAIL = "email";
    private static final String SURNAME = "surName";
    private static final String GIVENNAME = "givenName";
    private static final String MEMBEROF = "memberof";
    private static final String INITCONTENT = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"+
                                    "<"+SUBJECTS+">\n"+"<"+USERS+">\n"+"</"+USERS+">\n"+"<"+GROUPS+">\n"+"</"+GROUPS+">\n"+"</"+SUBJECTS+">\n";
   
    
    
    private static Log log = LogFactory.getLog(AuthFile.class);
    private static XMLConfiguration userpassword = null;
    private String authURI = null;
    private static String passwordFilePath = null;
    private static AuthFileHashInterface hashClass = null;
    private boolean readPathFromProperty = true;
    
    /**
     * Get the instance of the AuthFile
     * @return
     * @throws AuthenticationException
     */
    /*public static AuthFile getInstance() throws AuthenticationException {
        if(authFile == null) {
            authFile = new AuthFile();
        }
        return authFile;
    }*/
    
    /**
     * Get the instance of the AuthFile from specified password file
     * @return
     * @throws AuthenticationException
     */
    /*public static AuthFile getInstance(String passwordFile) throws AuthenticationException {
        passwordFilePath = passwordFile;
        if(authFile == null) {
            authFile = new AuthFile();
        }
        return authFile;
    }*/
    
    /**
     * Constructor
     */
    public AuthFile() throws AuthenticationException {
        try {
            init();
        } catch (Exception e) {
            e.printStackTrace();
            throw new AuthenticationException(e.getMessage());
        }
        
    }
    
    /**
     * 
     * @param passwordFile
     * @throws AuthenticationException
     */
    public  AuthFile (String passwordFile) throws AuthenticationException {
        passwordFilePath = passwordFile;
        readPathFromProperty = false;
        try {
            init();
        } catch (Exception e) {
            e.printStackTrace();
            throw new AuthenticationException(e.getMessage());
        }
    }
    
    /*
     * Initialize the user/password configuration
     */
    private void init() throws PropertyNotFoundException, IOException, ConfigurationException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        if(readPathFromProperty || passwordFilePath == null) {
            passwordFilePath  = PropertyService.getProperty("auth.file.path");
        }
        //System.out.println("the password file path is ======================= "+passwordFilePath);
        File passwordFile = new File(passwordFilePath);
        
        authURI = SystemUtil.getContextURL();
        String hashClassName = PropertyService.getProperty("auth.file.hashClassName");
        Class classDefinition = Class.forName(hashClassName);
        Object object = classDefinition.newInstance();
        hashClass = (AuthFileHashInterface) object;
        
        //if the password file doesn't exist, create a new one and set the initial content
        if(!passwordFile.exists()) {
            File parent = passwordFile.getParentFile();
            if(!parent.exists()) {
                boolean success = false;
                try {
                    success = parent.mkdirs();
                } catch (Exception e) {
                    throw new IOException("AuthFile.init - couldn't create the directory "+parent.getAbsolutePath()+ " since "+e.getMessage());
                }
                if(!success) {
                    throw new IOException("AuthFile.init - couldn't create the directory "+parent.getAbsolutePath()+ ", probably since the metacat doesn't have the write permission.");
                }
            }
            boolean success = false;
            try {
                success = passwordFile.createNewFile();
            }  catch (Exception e) {
                throw new IOException("AuthFile.init - couldn't create the file "+passwordFile.getAbsolutePath()+ " since "+e.getMessage());
            }
            if(!success) {
                throw new IOException("AuthFile.init - couldn't create the file "+parent.getAbsolutePath()+ ", probably since the metacat doesn't have the write permission.");
            }
            
            OutputStreamWriter writer = null;
            FileOutputStream output = null;
            try {
              output = new FileOutputStream(passwordFile);
              writer = new OutputStreamWriter(output, "UTF-8");
              writer.write(INITCONTENT);
            } finally {
              writer.close();
              output.close();
            }
          }
          userpassword = new XMLConfiguration(passwordFile);
          userpassword.setExpressionEngine(new XPathExpressionEngine());
          userpassword.setAutoSave(true);
          userpassword.setDelimiterParsingDisabled(true);
          userpassword.setAttributeSplittingDisabled(true);
    }
    
    @Override
    public boolean authenticate(String user, String password)
                    throws AuthenticationException {
        boolean match = false;
        String passwordRecord = userpassword.getString(USERS+SLASH+USER+"["+AT+DN+"='"+user+"']"+SLASH+PASSWORD);
        if(passwordRecord != null) {
            try {
                match = hashClass.match(password, passwordRecord);
            } catch (Exception e) {
                throw new AuthenticationException(e.getMessage());
            }
            
        }
        return match;
    }
    
    @Override
    /**
     * Get all users. This is two-dimmention array. Each row is a user. The first element of
     * a row is the user name. The second element is common name. The third one is the organization name (null).
     * The fourth one is the organization unit name (null). The fifth one is the email address.
     */
    public String[][] getUsers(String user, String password)
                    throws ConnectException {
        List<Object> users = userpassword.getList(USERS+SLASH+USER+SLASH+AT+DN);
        if(users != null && users.size() > 0) {
            String[][] usersArray = new String[users.size()][5];
            for(int i=0; i<users.size(); i++) {
                
                String dn = (String)users.get(i);
                usersArray[i][AuthInterface.USERDNINDEX] = dn; //dn
                String[] userInfo = getUserInfo(dn, password);
                usersArray[i][AuthInterface.USERCNINDEX] = userInfo[AuthInterface.USERINFOCNINDEX];//common name
                usersArray[i][AuthInterface.USERORGINDEX] = userInfo[AuthInterface.USERINFOORGANIDEX];//organization name. We set null
                usersArray[i][AuthInterface.USERORGUNITINDEX] = null;//organization ou name. We set null.
                usersArray[i][AuthInterface.USEREMAILINDEX] = userInfo[AuthInterface.USERINFOEMAILINDEX];
               
            }
            return usersArray;
        }
        return null;
    }
    
    @Override
    /**
     * Get an array about the user. The first column is the common name, the second column is the organization name.
     * The third column is the email address. It always returns an array. But the elements of the array can be null.
     */
    public String[] getUserInfo(String user, String password)
                    throws ConnectException {
        String[] userinfo = new String[3];
        User aUser = new User();
        aUser.setDN(user);
        String surname = null;
        List<Object> surNames = userpassword.getList(USERS+SLASH+USER+"["+AT+DN+"='"+user+"']"+SLASH+SURNAME);
        if(surNames != null && !surNames.isEmpty()) {
            surname = (String)surNames.get(0);
        }
        aUser.setSurName(surname);
        String givenName = null;
        List<Object> givenNames = userpassword.getList(USERS+SLASH+USER+"["+AT+DN+"='"+user+"']"+SLASH+GIVENNAME);
        if(givenNames != null && !givenNames.isEmpty()) {
            givenName = (String)givenNames.get(0);
        }
        aUser.setGivenName(givenName);
        userinfo[AuthInterface.USERINFOCNINDEX] = aUser.getCn();//common name
        String organization = null;
        List<Object> organizations = userpassword.getList(USERS+SLASH+USER+"["+AT+DN+"='"+user+"']"+SLASH+ORGANIZATION);
        if(organizations != null && !organizations.isEmpty()) {
            organization = (String)organizations.get(0);
        }
        userinfo[AuthInterface.USERINFOORGANIDEX] = organization;//organization name.
        aUser.setOrganization(organization);
        List<Object> emails = userpassword.getList(USERS+SLASH+USER+"["+AT+DN+"='"+user+"']"+SLASH+EMAIL);
        String email = null;
        if(emails != null && !emails.isEmpty() ) {
            email = (String)emails.get(0);
        }
        aUser.setEmail(email);
        userinfo[AuthInterface.USERINFOEMAILINDEX] = email;
        return userinfo;
    }
    
    
    @Override
    /**
     * Get the users for a particular group from the authentication service
     * The null will return if there is no user.
     * @param user
     *            the user for authenticating against the service
     * @param password
     *            the password for authenticating against the service
     * @param group
     *            the group whose user list should be returned
     * @returns string array of the user names belonging to the group
     */
    public String[] getUsers(String user, String password, String group)
                    throws ConnectException {
        List<Object> users = userpassword.getList(USERS+SLASH+USER+"["+MEMBEROF+"='"+group+"']"+SLASH+AT+DN);
        if(users != null && users.size() > 0) {
            String[] usersArray = new String[users.size()];
            for(int i=0; i<users.size(); i++) {
                usersArray[i] = (String) users.get(i);
            }
            return usersArray;
        }
        return null;
    }
    
    @Override
    /**
     * Get all groups from the authentication service. It returns a two dimmension array. Each row is a
     * group. The first column is the group name. The second column is the description. The null will return if no group found.
     */
    public String[][] getGroups(String user, String password)
                    throws ConnectException {
        List<Object> groups = userpassword.getList(GROUPS+SLASH+GROUP+SLASH+AT+NAME);
        if(groups!= null && groups.size() >0) {
            String[][] groupsArray = new String[groups.size()][2];
            for(int i=0; i<groups.size(); i++) {
                String groupName = (String) groups.get(i);
                groupsArray[i][AuthInterface.GROUPNAMEINDEX] = groupName;
                String description = null;
                List<Object>descriptions = userpassword.getList(GROUPS+SLASH+GROUP+"["+AT+NAME+"='"+groupName+"']"+SLASH+DESCRIPTION);
                if(descriptions != null && !descriptions.isEmpty()) {
                    description = (String)descriptions.get(0);
                }
                groupsArray[i][AuthInterface.GROUPDESINDEX] = description; 
            }
            return groupsArray;
        }
        return null;
    }
    
    @Override
    /**
     * Get groups from a specified user. It returns two dimmension array. Each row is a
     * group. The first column is the group name. The null will return if no group found.
     */
    public String[][] getGroups(String user, String password, String foruser)
                    throws ConnectException {
        List<Object> groups = userpassword.getList(USERS+SLASH+USER+"["+AT+DN+"='"+foruser+"']"+SLASH+MEMBEROF);
        if(groups != null && groups.size() > 0) {
            String[][] groupsArray = new String[groups.size()][2];
            for(int i=0; i<groups.size(); i++) {
                String groupName = (String) groups.get(i);
                groupsArray[i][AuthInterface.GROUPNAMEINDEX] = groupName;
                String description = null;
                List<Object>descriptions = userpassword.getList(GROUPS+SLASH+GROUP+"["+AT+NAME+"='"+groupName+"']"+SLASH+DESCRIPTION);
                if(descriptions != null && !descriptions.isEmpty()) {
                    description = (String)descriptions.get(0);
                }
                groupsArray[i][AuthInterface.GROUPDESINDEX] = description; 
            }
            return groupsArray;
        }
        return null;
    }
    
    @Override
    public HashMap<String, Vector<String>> getAttributes(String foruser)
                    throws ConnectException {
        //we only check if the user exists or not.
        if(!userExists(foruser)) {
            throw new ConnectException("NameNotFoundException - the user "+foruser+" doesn't exist in the password file.");
        }
        return null;
    }
    
    @Override
    public HashMap<String, Vector<String>> getAttributes(String user,
                    String password, String foruser) throws ConnectException {
        // TODO Auto-generated method stub
        return null;
    }
    
    @Override
    public String getPrincipals(String user, String password)
                    throws ConnectException {
            StringBuffer out = new StringBuffer();

            out.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
            out.append("<principals>\n");
            out.append("  <authSystem URI=\"" +authURI
                    + "\" organization=\"" + ORGANIZATIONNAME + "\">\n");

            // get all groups for directory context
            String[][] groups = getGroups(user, password);
            String[][] users = getUsers(user, password);
            int userIndex = 0;

            // for the groups and users that belong to them
            if (groups != null && users != null && groups.length > 0) {
                for (int i = 0; i < groups.length; i++) {
                    out.append("    <group>\n");
                    out.append("      <groupname>" + groups[i][AuthInterface.GROUPNAMEINDEX] + "</groupname>\n");
                    if(groups[i].length > 1) {
                        out.append("      <description>" + groups[i][AuthInterface.GROUPDESINDEX] + "</description>\n");
                    }
                    String[] usersForGroup = getUsers(user, password, groups[i][0]);
                    if(usersForGroup != null) {
                        for (int j = 0; j < usersForGroup.length; j++) {
                            userIndex = AuthLdap.searchUser(usersForGroup[j], users);
                            out.append("      <user>\n");

                            if (userIndex < 0) {
                                out.append("        <username>" + usersForGroup[j]
                                        + "</username>\n");
                            } else {
                                out.append("        <username>" + users[userIndex][0]
                                        + "</username>\n");
                                if(users[userIndex][AuthInterface.USERCNINDEX] != null) {
                                    out.append("        <name>" + users[userIndex][AuthInterface.USERCNINDEX]
                                                    + "</name>\n");
                                }
                                if(users[userIndex][AuthInterface.USERORGINDEX] != null) {
                                    out.append("        <organization>" + users[userIndex][AuthInterface.USERORGINDEX]
                                                    + "</organization>\n");
                                }
                                
                                if(users[userIndex][AuthInterface.USERORGUNITINDEX] != null) {
                                    out.append("      <organizationUnitName>"
                                                    + users[userIndex][AuthInterface.USERORGUNITINDEX]
                                                    + "</organizationUnitName>\n");
                                }
                                if(users[userIndex][AuthInterface.USEREMAILINDEX] != null) {
                                    out.append("        <email>" + users[userIndex][AuthInterface.USEREMAILINDEX]
                                                    + "</email>\n");
                                }
                               
                            }

                            out.append("      </user>\n");
                        }
                    }
                   
                    out.append("    </group>\n");
                }
            }

            if (users != null) {
                // for the users not belonging to any grou8p
                for (int j = 0; j < users.length; j++) {
                    out.append("    <user>\n");
                    out.append("      <username>" + users[j][0] + "</username>\n");
                    if(users[j][AuthInterface.USERCNINDEX] != null) {
                        out.append("        <name>" + users[j][AuthInterface.USERCNINDEX]
                                        + "</name>\n");
                    }
                    if(users[j][AuthInterface.USERORGINDEX] != null) {
                        out.append("        <organization>" + users[j][AuthInterface.USERORGINDEX]
                                        + "</organization>\n");
                    }
                    
                    if(users[j][AuthInterface.USERORGUNITINDEX] != null) {
                        out.append("      <organizationUnitName>"
                                        + users[j][AuthInterface.USERORGUNITINDEX]
                                        + "</organizationUnitName>\n");
                    }
                    if(users[j][AuthInterface.USEREMAILINDEX] != null) {
                        out.append("        <email>" + users[j][AuthInterface.USEREMAILINDEX]
                                        + "</email>\n");
                    }
                   
                    out.append("    </user>\n");
                }
            }

            out.append("  </authSystem>\n");
        
        out.append("</principals>");
        return out.toString();
    }
    
    /**
     * Add a user to the file
     * @param userName the name of the user
     * @param groups  the groups the user belong to. The group should exist in the file
     * @param password  the password of the user
     */
    public void addUser(String dn, String[] groups, String plainPass, String hashedPass, String email, String surName, String givenName, String organization) throws AuthenticationException{
       User user = new User();
       user.setDN(dn);
       user.setGroups(groups);
       user.setPlainPass(plainPass);
       user.setHashedPass(hashedPass);
       user.setEmail(email);
       user.setSurName(surName);
       user.setGivenName(givenName);
       user.setOrganization(organization);
       user.serialize();
    }
    
    /**
     * Add a group into the file
     * @param groupName the name of group
     */
    public void addGroup(String groupName, String description) throws AuthenticationException{
        if(groupName == null || groupName.trim().equals("")) {
            throw new AuthenticationException("AuthFile.addGroup - can't add a group whose name is null or blank.");
        }
        if(!groupExists(groupName)) {
            if(userpassword != null) {
              userpassword.addProperty(GROUPS+" "+GROUP+AT+NAME, groupName);
              if(description != null && !description.trim().equals("")) {
                  userpassword.addProperty(GROUPS+SLASH+GROUP+"["+AT+NAME+"='"+groupName+"']"+" "+DESCRIPTION, description);
              }
              //userpassword.reload();
             }
        } else {
            throw new AuthenticationException("AuthFile.addGroup - can't add the group "+groupName+" since it already exists.");
        }
    }
    
   
    
    /**
     * Change the password of the user to the new one which is hashed
     * @param usrName the specified user.   
     * @param newPassword the new password which will be set
     */
    public void modifyPassWithHash(String userName, String newHashPassword) throws AuthenticationException {
       User user = new User();
       user.setDN(userName);
       user.modifyHashPass(newHashPassword);
    }
    
    /**
     * Change the password of the user to the new one which is plain. However, only the hashed version will be serialized.
     * @param usrName the specified user.   
     * @param newPassword the new password which will be set
     */
    public void modifyPassWithPlain(String userName, String newPlainPassword) throws AuthenticationException {
        User user = new User();
        user.setDN(userName);
        user.modifyPlainPass(newPlainPassword);
    }
    
    
    /**
     * Add a user to a group
     * @param userName  the name of the user. the user should already exist
     * @param group  the name of the group. the group should already exist
     */
    public void addUserToGroup(String userName, String group) throws AuthenticationException {
        User user = new User();
        user.setDN(userName);
        user.addToGroup(group);
    }
    
    /**
     * Remove a user from a group.
     * @param userName  the name of the user. the user should already exist.
     * @param group the name of the group
     */
    public void removeUserFromGroup(String userName, String group) throws AuthenticationException{
        User user = new User();
        user.setDN(userName);
        user.removeFromGroup(group);
    }
    
  
    
    /**
     * If the specified user name exist or not
     * @param userName the name of the user
     * @return true if the user eixsit
     */
    private synchronized boolean userExists(String userName) throws AuthenticationException{
        if(userName == null || userName.trim().equals("")) {
            throw new AuthenticationException("AuthFile.userExist - can't judge if a user exists when its name is null or blank.");
        }
        List<Object> users = userpassword.getList(USERS+SLASH+USER+SLASH+AT+DN);
        if(users != null && users.contains(userName)) {
            return true;
        } else {
            return false;
        }
    }
    
    /**
     * If the specified group exist or not
     * @param groupName the name of the group
     * @return true if the user exists
     */
    private synchronized boolean groupExists(String groupName) throws AuthenticationException{
        if(groupName == null || groupName.trim().equals("")) {
            throw new AuthenticationException("AuthFile.groupExist - can't judge if a group exists when its name is null or blank.");
        }
        List<Object> groups = userpassword.getList(GROUPS+SLASH+GROUP+SLASH+AT+NAME);
        if(groups != null && groups.contains(groupName)) {
            return true;
        } else {
            return false;
        }
    }
    
    /*
     * Encrypt a plain text
     */
    private static String encrypt(String plain)  {
      return hashClass.hash(plain);
    }
    
    
    /**
     * A method is used to help administrator to manage users and groups
     * @param argus
     * @throws Exception
     */
    public static void main(String[] argus) throws Exception {
            String USERADD = "useradd";
            String USERMOD = "usermod";
            String GROUPADD = "groupadd";
            String USAGE = "usage";
            if(argus == null || argus.length ==0) {
              System.out.println("Please make sure that there are two arguments - \"$BASE_WEB_INF\" and\" $@\" after the class name edu.ucsb.nceas.metacat.authentication.AuthFile in the script file.");
              System.exit(1);
            } else if(argus.length ==1) {
                printUsage();
                System.exit(1);
            }
            PropertyService.getInstance(argus[0]);
            AuthFile authFile = new AuthFile();
            if(argus[1] != null && argus[1].equals(GROUPADD)) {
                handleGroupAdd(authFile,argus);
            } else if (argus[1] != null && argus[1].equals(USERADD)) {
                handleUserAdd(authFile,argus);
            } else if (argus[1] != null && argus[1].equals(USERMOD)) {
                handleUserMod(authFile, argus);
            } else if (argus[1] != null && argus[1].equals(USAGE)) {
                printUsage();
            } else {
                System.out.print("Error: the unknown action "+argus[1]);
            }
    }
    
    /*
     * Handle the groupAdd action in the main method
     */
    private static void handleGroupAdd(AuthFile authFile, String[]argus) throws AuthenticationException {
        HashMap<String, String> map = null;
        String G = "-g";
        String D = "-d";
        Vector<String> pairedOptions = new Vector<String>();
        pairedOptions.add(G);
        pairedOptions.add(D);
        int startIndex = 2;
        try {
            map = parseArgus(startIndex, argus, pairedOptions, null);
        } catch (Exception e ) {
            System.out.println("Error in the groupadd command: "+e.getMessage());
            System.exit(1);
        }
        String groupName = null;
        String description = null;
        if(map.keySet().size() == 0) {
            System.out.println("Error in the groupadd command: the \""+G+" group-name\" is required.");
            System.exit(1);
        } else if(map.keySet().size() ==1 || map.keySet().size() ==2) {
            groupName = map.get(G);
            if(groupName == null || groupName.trim().equals("")) {
                System.out.println("Error in the groupadd command : the \""+G+" group-name\" is required.");
                System.exit(1);
            }
            description = map.get(D);
            authFile.addGroup(groupName, description);
            System.out.println("Successfully added a group \""+groupName+"\" to the file authentication system");
        } else {
            printError(argus);
            System.exit(1);
        }
    }
    
    /*
     * Handle the userAdd action in the main method
     */
    private static void  handleUserAdd(AuthFile authFile,String[]argus) throws UnsupportedEncodingException, AuthenticationException{
      
        String I = "-i";
        String H = "-h";
        String DN = "-dn";
        String G = "-g";
        String E = "-e";
        String S = "-s";
        String F = "-f";
        String O = "-o";
        Vector<String> pairedOptions = new Vector<String>();
        pairedOptions.add(H);
        pairedOptions.add(DN);
        pairedOptions.add(G);
        pairedOptions.add(E);
        pairedOptions.add(S);
        pairedOptions.add(F);
        pairedOptions.add(O);
        Vector<String> singleOptions = new Vector<String>();
        singleOptions.add(I);
        
        HashMap<String, String> map = new HashMap<String, String>();
        int startIndex = 2;
        try {
            map = parseArgus(startIndex, argus, pairedOptions, singleOptions);
        } catch (Exception e) {
            System.out.println("Error in the useradd command: "+e.getMessage());
            System.exit(1);
        }
       
        String dn = map.get(DN);
        if(dn == null || dn.trim().equals("")) {
            System.out.println("The \"-dn user-distinguish-name\" is requried in the useradd command ."); 
            System.exit(1);
        } 
        String plainPassword = null;
        String hashedPassword = null;
       
        
        String input = map.get(I);
        String passHash = map.get(H);
        if(input != null && passHash != null) {
            System.out.println("Error in the useradd command: you only can choose either \"-i\" (input a password) or \"-h hashed-password\" (pass through a hashed passwword).");
            System.exit(1);
        } else if (input == null && passHash == null) {
            System.out.println("Error in the useradd command: you must choose either \"-i\" (input a password) or \"-h hashed-password\" (pass through a hashed password).");
            System.exit(1);
        } else if(input != null) {
            plainPassword = inputPassword();
            //System.out.println("============the plain password is "+plainPassword);
        } else if(passHash != null) {
            hashedPassword = passHash;
        }
        
        String group = map.get(G);
        //System.out.println("the groups name is "+group);
        String[] groups = null;
        if(group != null && !group.trim().equals("")) {
            groups = new String[1];
            groups[0]=group;
            //System.out.println("set the first element of the groups to "+groups[0]);
        }
        String email = map.get(E);
        String surname = map.get(S);
        String givenname = map.get(F);
        String organization = map.get(O);
        authFile.addUser(dn, groups, plainPassword, hashedPassword, email, surname, givenname, organization);
        System.out.println("Successfully added a user \""+dn+"\" to the file authentication system ");
    }
    
    /*
     * Handle modify a user's password or group information.
     */
    private static void handleUserMod(AuthFile authFile, String[] argus) throws AuthenticationException, UnsupportedEncodingException {
        String PASSWORD = "-password";
        String GROUP = "-group";
        if(argus.length < 3) {
            System.out.println("Error: the sub action \"-password\" or \"-group\" should follow the action \"usermod\"");
            System.exit(1);
        } else {
            if(argus[2] != null && argus[2].equals(PASSWORD)) {
                handleModifyPass(authFile, argus);
            } else if (argus[2] != null && argus[2].equals(GROUP)) {
                handleModifyGroup(authFile, argus);
            } else {
                System.out.println("Error: the sub action \""+argus[2]+"\" is unkown in the action \"usermod\"");
                System.exit(1);
            }
        }
    }
    
    /*
     * Handle the action to modify the password of a user
     */
    private static void handleModifyPass(AuthFile authFile, String[] argus) throws UnsupportedEncodingException, AuthenticationException {
        String DN = "-dn";
        String I = "-i";
        String H = "-h";
        Vector<String> pairedOptions = new Vector<String>();
        pairedOptions.add(H);
        pairedOptions.add(DN);
        Vector<String> singleOptions = new Vector<String>();
        singleOptions.add(I);
        HashMap<String, String> map = new HashMap<String, String>();
        int startIndex = 3;
        try {
            map = parseArgus(startIndex, argus, pairedOptions, singleOptions);
        } catch (Exception e) {
            System.out.println("Error in the usermod -password command: "+e.getMessage());
            System.exit(1);
        }
      
        String dn = map.get(DN);
        if(dn == null || dn.trim().equals("")) {
            System.out.println("Error in the usermod -password command: The \"-dn user-distinguish-name\" is requried."); 
            System.exit(1);
        }
        String plainPassword = null;
        String hashedPassword = null;
        
        String input = map.get(I);
        String passHash = map.get(H);
        if(input != null && passHash != null) {
            System.out.println("Error in the usermod -password command: you only can choose either \"-i\" (input a password) or \"-h hashed-password\" (pass through a hashed password).");
            System.exit(1);
        } else if (input == null && passHash == null) {
            System.out.println("Error in the usermod -password command: you must choose either \"-i\" (input a password) or \"-h hashed-password\" (pass through a hashed password).");
            System.exit(1);
        } else if(input != null) {
            plainPassword = inputPassword();
            //System.out.println("============the plain password is "+plainPassword);
            authFile.modifyPassWithPlain(dn, plainPassword);
            System.out.println("Successfully modified the password for the user \""+dn+"\".");
        } else if(passHash != null) {
            hashedPassword = passHash;
            authFile.modifyPassWithHash(dn, hashedPassword);
            System.out.println("Successfully modified the password for the user "+dn+"\".");
        }
    }
    
    /*
     * Handle the action adding/removing a user to/from a group
     */
    private static void handleModifyGroup(AuthFile authFile, String[] argus) throws AuthenticationException {
        String DN = "-dn";
        String A = "-a";
        String R = "-r";
        String G = "-g";
        Vector<String> pairedOptions = new Vector<String>();
        pairedOptions.add(G);
        pairedOptions.add(DN);
        Vector<String> singleOptions = new Vector<String>();
        singleOptions.add(A);
        singleOptions.add(R);
        HashMap<String, String> map = new HashMap<String, String>();
        int startIndex = 3;
        try {
            map = parseArgus(startIndex, argus, pairedOptions, singleOptions);
        } catch (Exception e) {
            System.out.println("Error in the usermod -group command: "+e.getMessage());
            System.exit(1);
        }
               
        String add = map.get(A);
        String remove = map.get(R);
        String group = map.get(G);
        String dn = map.get(DN);
        if(dn == null || dn.trim().equals("")) {
            System.out.println("Error in the usermod -group command: the \"-dn user-distinguish-name\" is required.");
            System.exit(1);
        }
        
        if(group == null || group.trim().equals("")) {
            System.out.println("Error in the usermod -group command: the \"-g group-name\" is required.");
            System.exit(1);
        }
        
        if(add != null && remove!= null) {
            System.out.println("Error in the usermod -group command: You can only choose either \"-a\" (add the user to the group) or \"-r\" (remove the user from the group).");
            System.exit(1);
        } else if (add == null && remove == null) {
            System.out.println("Error in the usermod -group command: You must choose either \"-a\" (add the user to the group) or \"-r\" (remove the user from the group).");
            System.exit(1);
        } else if (remove != null) {
            authFile.removeUserFromGroup(dn, group);
            System.out.println("Successfully removed the user "+dn+" from the group \""+group+"\".");
        } else {
            authFile.addUserToGroup(dn, group);
            System.out.println("Successfully added the user "+dn+" to the group \""+group+"\".");
        }
    }
    
    
    /**
     * Parse the arguments to get the pairs of option/value. If it is a single option (it doesn't need a value), the pair will be switch/switch. 
     * @param startIndex the index of arguments where we will start.
     * @param argus the arguments array will be parsed.
     * @param pairedOptions the known options which should be a pair
     * @param singleOptions the know options which just has a single value
     * @return the empty map if there is no pairs were found
     * @throws Exception if there is an illegal argument.
     */
    private static HashMap<String, String> parseArgus(int startIndex, String[]argus, Vector<String>pairedOptions, Vector<String>singleOptions) throws Exception {
        HashMap<String, String> map = new HashMap<String, String>();
        if(argus != null) {
            for(int i=startIndex; i<argus.length; i++) {
                String arg = argus[i];
                if(map.containsKey(arg)) {
                    throw new Exception("The command line can't have duplicate options \""+arg+"\".");
                }
                
                if(singleOptions != null && singleOptions.contains(arg)) {
                    //we find a single option
                    if(i==argus.length-1) {
                        //it is the last argument, this is fine.
                        map.put(arg, arg);
                    } else if (i<argus.length -1) {
                        //it is not the last argument. 
                        if ((pairedOptions != null && pairedOptions.contains(argus[i+1])) || singleOptions.contains(argus[i+1])) {
                            //it follows an option, this is fine
                            map.put(arg, arg);
                        } else {
                            //it follows a vlaue, this is illegal
                            throw new Exception("The option \""+arg+"\" shouldn't be followed any value, e.g. "+ argus[i+1]+".");
                        }
                    }
                } else if (pairedOptions != null && pairedOptions.contains(arg)) {
                    //we found an option which should follow a vlaue
                    if(i==argus.length-1) {
                        //it is the last argument (no value follows it)
                        throw new Exception("The option \""+arg+"\" must be followed by a value");
                    } else {
                        //it is not the last argument and we need to check its following value
                        if (!pairedOptions.contains(argus[i+1]) && (singleOptions == null || !singleOptions.contains(argus[i+1]))) {
                            //it is NOT followed by an option, this is fine
                            map.put(arg, argus[i+1]);
                        } else {
                            //it is followed by an option, this is illegal
                            throw new Exception("The option \""+arg+"\" shouldn't be followed the option \""+ argus[i+1]+"\". It should be followed by a value.");
                        }
                    }
                } else {
                    //we found an argument is not an option
                    if(pairedOptions == null || !pairedOptions.contains(argus[i-1])) {
                        //the previous argument is not an option switch
                        throw new Exception("The \""+arg+"\" is an illegal argument");
                    }
                }
            } 
        }
        return map;
    }
    
    /*
     * Input the password
     */
    private static String inputPassword() throws UnsupportedEncodingException {
        String password = null;
        String quit = "q";
        Console console = System.console();
        if (console == null) {
            System.out.println("Sorry, we can't fetch the console from the system. You can't use the option \"-i\" to input a password. You have to use the option \"-h hashed-password\" to pass through a hashed passwprd in the useradd command. ");
            System.exit(1);
        }
  
        while(true) {
                System.out.print("Enter your new password (input 'q' to quit): ");
                String password1 = new String(console.readPassword());
                if(password1== null || password1.trim().equals("")) {
                    System.out.println("Error: the password can't be blank or null. Please try again.");
                    continue;
                } else if (password1.equals(quit)) {
                    System.exit(0);
                }
                System.out.print("Confirm your new password (input 'q' to quit): ");
                String password2 = new String(console.readPassword());
                if(password2 == null || password2.trim().equals("")) {
                    System.out.println("Error: the password can't be blank or null. Please try again.");
                    continue;
                }  else if (password2.equals(quit)) {
                    System.exit(0);
                }
                
                if(!password1.equals(password2)) {
                    System.out.println("Error: The second password does't match the first one. Please try again.");
                } else {
                    password = password1;
                    break;
                }
                
            
        }
        
        return password;
        
    }
    /*
     * Print out the usage statement
     */
    private static void printUsage() {
        System.out.println("Usage:\n"+
                        "./authFileManager.sh useradd -i -dn <user-distinguish-name> [-g <group-name> -e <email-address> -s <surname> -f <given-name> -o <organizationName>]\n" +
                        "./authFileManager.sh useradd -h <hashed-password> -dn <user-distinguish-name> [-g <group-name> -e <email-address> -s <surname> -f <given-name> -o <organizationName>]\n"+
                        "./authFileManager.sh groupadd -g group-name [-d description]\n" +
                        "./authFileManager.sh usermod -password -dn <user-distinguish-name> -i\n"+
                        "./authFileManager.sh usermod -password -dn <user-distinguish-name> -h <new-hashed-password>\n"+
                        "./authFileManager.sh usermod -group -a -dn <user-distinguish-name> -g <added-group-name>\n" +
                        "./authFileManager.sh usermod -group -r -dn <user-distinguish-name> -g <removed-group-name>\n"+
                        "Note:\n"+"1. Metacat currently uses Bcrypt algorithm to hash the password. The hashed password following the \"-h\" should be generated by a Bcrypt algorithm.\n"+
                        "  The hash string usually has $ signs which can interfere with the command line arguments. You should use two SINGLE quotes to wrap the entire hashed string.\n"+
                        "2. The user-distinguish-name must look like \"uid=john,o=something,dc=something,dc=something\" and the group-name must look like \"cn=dev,o=something,dc=something,dc=something\".\n"+
                        "3. if a value of an option has spaces, the value should be enclosed by the double quotes.\n"+
                        "  For example: ./authFileManager.sh groupadd -g cn=dev,o=something,dc=something,dc=something -d \"Developers at NCEAS\"\n"+
                        "4. \"-d description\" in the \"groupadd\" command is optional; \"-g groupname -e email-address -s surname -f given-name -o organizationName\" in the \"useradd\" command are optional as well.");
                       
                        
    }
    
    /*
     * Print out the statement to say it is a illegal command
     */
    private static void printError(String[] argus) {
        if(argus != null) {
            System.out.println("Error: it is an illegal command (probably with some illegal options): ");
            for(int i=0; i<argus.length; i++) {
                if(i!= 0) {
                    System.out.print(argus[i]+" ");
                }
            }
            System.out.println("");
        }
       
    }

    
    /**
     * An class represents the information for a user. 
     * @author tao
     *
     */
    private class User {
        private String dn = null;//the distinguish name
        private String plainPass = null;
        private String hashedPass = null;
        private String email = null;
        private String surName = null;
        private String givenName = null;
        private String cn = null;//the common name
        private String[] groups = null;
        private String organization = null;
        
        /**
         * Get the organization of the user
         * @return
         */
        public String getOrganization() {
            return organization;
        }
        
        /**
         * Set the organization for the user.
         * @param organization
         */
        public void setOrganization(String organization) {
            this.organization = organization;
        }
        /**
         * Get the distinguish name of the user
         * @return the distinguish name 
         */
        public String getDN() {
            return this.dn;
        }
        
        /**
         * Set the distinguish name for the user
         * @param dn the specified dn
         */
        public void setDN(String dn) {
            this.dn = dn;
        }
        
        /**
         * Get the plain password for the user. This value will NOT be serialized to
         * the password file
         * @return the plain password for the user
         */
        public String getPlainPass() {
            return plainPass;
        }
        
        /**
         * Set the plain password for the user.
         * @param plainPass the plain password will be set.
         */
        public void setPlainPass(String plainPass) {
            this.plainPass = plainPass;
        }
        
        /**
         * Get the hashed password of the user
         * @return the hashed password of the user
         */
        public String getHashedPass() {
            return hashedPass;
        }
        
        /**
         * Set the hashed the password for the user.
         * @param hashedPass the hashed password will be set.
         */
        public void setHashedPass(String hashedPass) {
            this.hashedPass = hashedPass;
        }
        
        /**
         * Get the email of the user
         * @return the email of the user
         */
        public String getEmail() {
            return email;
        }
        
        /**
         * Set the email address for the user
         * @param email the eamil address will be set
         */
        public void setEmail(String email) {
            this.email = email;
        }
        
        /**
         * Get the surname of the user
         * @return the surname of the user
         */
        public String getSurName() {
            return surName;
        }
        
        /**
         * Set the surname of the user
         * @param surName
         */
        public void setSurName(String surName) {
            this.surName = surName;
        }
        
        /**
         * Get the given name of the user
         * @return the given name of the user
         */
        public String getGivenName() {
            return givenName;
        }
        
        /**
         * Set the GivenName of the user
         * @param givenName
         */
        public void setGivenName(String givenName) {
            this.givenName = givenName;
        }
        
        /**
         * Get the common name of the user. If the cn is null, the GivenName +SurName will
         * be returned
         * @return the common name
         */
        public String getCn() {
            if(cn != null) {
                return cn;
            } else {
                if (givenName != null && surName != null) {
                    return givenName+" "+surName;
                } else if (givenName != null) {
                    return givenName;
                } else if (surName != null ) {
                    return surName;
                } else {
                    return null;
                }
            }
        }
        
        /**
         * Set the common name for the user
         * @param cn
         */
        public void setCn(String cn) {
            this.cn = cn;
        }
        
        /**
         * Get the groups of the user belong to
         * @return
         */
        public String[] getGroups() {
            return groups;
        }
        
        /**
         * Set the groups of the user belong to
         * @param groups
         */
        public void setGroups(String[] groups) {
            this.groups = groups;
        }
        
        /**
         * Add the user to a group and serialize the change to the password file.
         * @param group the group which the user will join
         * @throws AuthenticationException 
         */
        public void addToGroup(String group) throws AuthenticationException {
            if(group == null || group.trim().equals("")) {
                throw new IllegalArgumentException("AuthFile.User.addToGroup - the group can't be null or blank");
            }
            if(!userExists(dn)) {
                throw new AuthenticationException("AuthFile.User.addUserToGroup - the user "+dn+ " doesn't exist.");
            }
            if(!groupExists(group)) {
                throw new AuthenticationException("AuthFile.User.addUserToGroup - the group "+group+ " doesn't exist.");
            }
            List<Object> existingGroups = userpassword.getList(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+SLASH+MEMBEROF);
            if(existingGroups != null && existingGroups.contains(group)) {
                throw new AuthenticationException("AuthFile.User.addUserToGroup - the user "+dn+ " already is the memember of the group "+group);
            }
            userpassword.addProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+" "+MEMBEROF, group);
            //add information to the memory
            if(groups == null) {
                if(existingGroups == null || existingGroups.isEmpty()) {
                    groups = new String[1];
                    groups[0] = group;
                } else {
                    groups = new String[existingGroups.size()+1];
                    for(int i=0; i<existingGroups.size(); i++) {
                        groups[i] = (String)existingGroups.get(i);
                    }
                    groups[existingGroups.size()] = group;
                }
                
            } else {
                String[] oldGroups = groups;
                groups = new String[oldGroups.length+1];
                for(int i=0; i<oldGroups.length; i++) {
                    groups[i]= oldGroups[i];
                }
                groups[oldGroups.length] = group;
                
            }
        }
        
        /**
         * Remove the user from a group and serialize the change to the password file
         * @param group
         * @throws AuthenticationException
         */
        public void removeFromGroup(String group) throws AuthenticationException {
            if(!userExists(dn)) {
                throw new AuthenticationException("AuthFile.User.removeUserFromGroup - the user "+dn+ " doesn't exist.");
            }
            if(!groupExists(group)) {
                throw new AuthenticationException("AuthFile.User.removeUserFromGroup - the group "+group+ " doesn't exist.");
            }
            String key = USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+SLASH+MEMBEROF;
            List<Object> existingGroups = userpassword.getList(key);
            if(!existingGroups.contains(group)) {
                throw new AuthenticationException("AuthFile.User.removeUserFromGroup - the user "+dn+ " isn't the memember of the group "+group);
            } else {
                userpassword.clearProperty(key+"[.='"+group+"']");
            }
            //change the value in the memory.
            if(groups != null) {
                boolean contains = false;
                for(int i=0; i<groups.length; i++) {
                    if(groups[i].equals(group)) {
                        contains = true;
                        break;
                    }
                }
                String[] newGroups = new String[groups.length-1];
                int k =0;
                for(int i=0; i<groups.length; i++) {
                    if(!groups[i].equals(group)) {
                       newGroups[k] = groups[i];
                       k++;
                    }
                }
                groups = newGroups;
            }
        }
        
        /**
         * Modify the hash password and serialize it to the password file
         * @param hashPass
         * @throws AuthenticationException
         */
        public void modifyHashPass(String hashPass) throws AuthenticationException {
            if(hashPass == null || hashPass.trim().equals("")) {
                throw new AuthenticationException("AuthFile.User.modifyHashPass - can't change the password to the null or blank.");
            }
            if(!userExists(dn)) {
                throw new AuthenticationException("AuthFile.User.modifyHashPass - can't change the password for the user "+dn+" since it doesn't eixt.");
            }
            userpassword.setProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+SLASH+PASSWORD, hashPass);
            setHashedPass(hashPass);
      
        }
        
        /**
         * Modify the plain password and serialize its hash version to the password file
         * @param plainPass
         * @throws AuthenticationException 
         */
        public void modifyPlainPass(String plainPass) throws AuthenticationException {
            if(plainPass == null || plainPass.trim().equals("")) {
                throw new AuthenticationException("AuthFile.User.modifyPlainPass - can't change the password to the null or blank.");
            }
            if(!userExists(dn)) {
                throw new AuthenticationException("AuthFile.User.modifyPlainPass - can't change the password for the user "+dn+" since it doesn't eixt.");
            }
            String hashPassword = null;
            try {
                hashPassword = encrypt(plainPass);
            } catch (Exception e) {
                throw new AuthenticationException("AuthFile.User.modifyPlainPass - can't encript the password since "+e.getMessage());
            }
            userpassword.setProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+SLASH+PASSWORD, hashPassword);
            setPlainPass(plainPass);
        }
        
        /**
         * Add the user to the password file. 
         */
        public void serialize() throws AuthenticationException {
            if(dn == null || dn.trim().equals("")) {
                throw new AuthenticationException("AuthFile.User.serialize - can't add a user whose name is null or blank.");
            }
            if(hashedPass == null || hashedPass.trim().equals("")) {
                if(plainPass == null || plainPass.trim().equals("")) {
                    throw new AuthenticationException("AuthFile.User.serialize - can't add a user whose password is null or blank.");
                } else {
                    try {
                        hashedPass = encrypt(plainPass);
                    } catch (Exception e) {
                        throw new AuthenticationException("AuthFile.User.serialize - can't encript the password since "+e.getMessage());
                    }
                }
            }
            if(groups != null) {
                for(int i=0; i<groups.length; i++) {
                    String group = groups[i];
                    if(group != null && !group.trim().equals("")) {
                        if(!groupExists(group)) {
                            throw new AuthenticationException("AuthFile.User.serialize - can't put the user into a non-existing group "+group);
                        }
                    }
                }
            }

            if(!userExists(dn)) {
                if(userpassword != null) {
                  userpassword.addProperty(USERS+" "+USER+AT+DN, dn);
                  userpassword.addProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+" "+PASSWORD, hashedPass);
                  
                  if(email != null && !email.trim().equals("")) {
                      userpassword.addProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+" "+EMAIL, email);
                  }
                  
                  if(surName != null && !surName.trim().equals("")) {
                      userpassword.addProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+" "+SURNAME, surName);
                  }
                  
                  if(givenName != null && !givenName.trim().equals("")) {
                      userpassword.addProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+" "+GIVENNAME, givenName);
                  }
                  
                  if(organization != null && !organization.trim().equals("")) {
                      userpassword.addProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+" "+ORGANIZATION, organization);
                  }

                  if(groups != null) {
                      for(int i=0; i<groups.length; i++) {
                          String group = groups[i];
                          if(group != null && !group.trim().equals("")) {
                              if(groupExists(group)) {
                                  userpassword.addProperty(USERS+SLASH+USER+"["+AT+DN+"='"+dn+"']"+" "+MEMBEROF, group);
                              }
                          }
                      }
                  }
                  //userpassword.reload();
                 }
            } else {
                throw new AuthenticationException("AuthFile.User.serialize - can't add the user "+dn+" since it already exists.");
            }
        }
    }

}