package org.greenstone.gsdl3.service;

import java.io.File;
import java.io.Serializable;
import java.math.BigInteger;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.UUID;
import java.util.Vector;
import java.util.regex.Pattern;

import org.greenstone.gsdl3.util.AuthenticationHelper;
import org.greenstone.gsdl3.util.DerbyWrapper;
import org.greenstone.gsdl3.util.GroupsUtil;
import org.greenstone.gsdl3.util.GSXML;
import org.greenstone.gsdl3.util.RecaptchaUtil;
import org.greenstone.gsdl3.util.UserQueryResult;
import org.greenstone.gsdl3.util.UserTermInfo;
import org.greenstone.gsdl3.util.XMLConverter;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class Authentication extends ServiceRack
{
	//Some useful constants
	protected static final int USERNAME_MIN_LENGTH = 2;
	protected static final int USERNAME_MAX_LENGTH = 30;
	protected static final int PASSWORD_MIN_LENGTH = 3;
	protected static final int PASSWORD_MAX_LENGTH = 64;

	//Error codes
	public static final int NO_ERROR = 0;
	protected static final int ERROR_NOT_LOGGED_IN = -1;
	protected static final int ERROR_ADMIN_NOT_LOGGED_IN = -2;
	protected static final int ERROR_COULD_NOT_GET_USER_INFO = -3;
	protected static final int ERROR_USERNAME_NOT_SPECIFIED = -4;
	protected static final int ERROR_USER_NOT_FOUND = -5;
	protected static final int ERROR_USER_NOT_AUTHORISED = -6;
	protected static final int ERROR_INVALID_USERNAME = -7;
	protected static final int ERROR_USER_ALREADY_EXISTS = -8;
	protected static final int ERROR_PASSWORD_NOT_ENTERED = -9;
	protected static final int ERROR_PASSWORD_TOO_SHORT = -10;
	protected static final int ERROR_PASSWORD_TOO_LONG = -11;
	protected static final int ERROR_PASSWORD_USES_ILLEGAL_CHARACTERS = -12;
	protected static final int ERROR_INCORRECT_PASSWORD = -13;
	protected static final int ERROR_ADDING_USER = -14;
	protected static final int ERROR_REMOVING_USER = -15;
        protected static final int ERROR_MISSING_PARAMS = -16;
        protected static final int ERROR_SOMETHING_WRONG = -17;


    protected static final HashMap<Integer, String> _errorKeyMap;
	static
	{
		//Corresponding error message keys for looking up in ServiceRack dictionary
		HashMap<Integer, String> errorKeyMap = new HashMap<Integer, String>();
		errorKeyMap.put(ERROR_NOT_LOGGED_IN, "auth.error.not_logged_in");
		errorKeyMap.put(ERROR_ADMIN_NOT_LOGGED_IN, "auth.error.admin_not_logged_in");
		errorKeyMap.put(ERROR_COULD_NOT_GET_USER_INFO, "auth.error.could_not_get_user_info");
		errorKeyMap.put(ERROR_USERNAME_NOT_SPECIFIED, "auth.error.username_not_specified");
		errorKeyMap.put(ERROR_USER_NOT_FOUND, "auth.error.user_not_found");
		errorKeyMap.put(ERROR_USER_NOT_AUTHORISED, "auth.error.not_authorised");
		errorKeyMap.put(ERROR_INVALID_USERNAME, "auth.error.invalid_username");
		errorKeyMap.put(ERROR_USER_ALREADY_EXISTS, "auth.error.user_already_exists");
		errorKeyMap.put(ERROR_PASSWORD_NOT_ENTERED, "auth.error.no_password");
		errorKeyMap.put(ERROR_PASSWORD_TOO_SHORT, "auth.error.password_too_short");
		errorKeyMap.put(ERROR_PASSWORD_TOO_LONG, "auth.error.password_too_long");
		errorKeyMap.put(ERROR_PASSWORD_USES_ILLEGAL_CHARACTERS, "auth.error.password_illegal_chars");
		errorKeyMap.put(ERROR_INCORRECT_PASSWORD, "auth.error.incorrect_password");
		errorKeyMap.put(ERROR_ADDING_USER, "auth.error.add_user_error");
		errorKeyMap.put(ERROR_REMOVING_USER, "auth.error.remove_user_error");
		errorKeyMap.put(ERROR_MISSING_PARAMS, "auth.error.missing_params"); // ???
		errorKeyMap.put(ERROR_SOMETHING_WRONG, "auth.error.something_wrong");
                // add in recaptcha error keys
		errorKeyMap.put(RecaptchaUtil.ERROR_CAPTCHA_FAILED, "auth.error.captcha_failed");
		errorKeyMap.put(RecaptchaUtil.ERROR_CAPTCHA_MISSING, "auth.error.captcha_missing");
		errorKeyMap.put(RecaptchaUtil.ERROR_CONNECTION_FAILED, "auth.error.connection_failed");

		_errorKeyMap = errorKeyMap;
	}

	//Admin-required operations
	protected static final String LIST_USERS = "ListUsers";
	protected static final String PERFORM_ADD = "PerformAdd";
	protected static final String PERFORM_EDIT = "PerformEdit";
	protected static final String ADD_USER = "AddUser";
	protected static final String EDIT_USER = "EditUser";
	protected static final String PERFORM_DELETE_USER = "PerformDeleteUser";

	protected static final ArrayList<String> _adminOpList;
	static
	{
		ArrayList<String> opList = new ArrayList<String>();
		opList.add(LIST_USERS);
		opList.add(PERFORM_ADD);
		opList.add(PERFORM_EDIT);
		opList.add(ADD_USER);
		opList.add(EDIT_USER);
		opList.add(PERFORM_DELETE_USER);

		_adminOpList = opList;
	}

	//User-required operations
	protected static final String ACCOUNT_SETTINGS = "AccountSettings";
	protected static final String PERFORM_ACCOUNT_EDIT = "PerformAccEdit";
	protected static final String PERFORM_RESET_PASSWORD = "PerformResetPassword";
	protected static final String PERFORM_CHANGE_PASSWORD = "PerformChangePassword";
	protected static final String PERFORM_RETRIEVE_PASSWORD = "PerformRetrievePassword";
	protected static final ArrayList<String> _userOpList;
	static
	{
		ArrayList<String> opList = new ArrayList<String>();
		opList.add(ACCOUNT_SETTINGS);
		opList.add(PERFORM_ACCOUNT_EDIT);
		opList.add(PERFORM_RESET_PASSWORD);
		opList.add(PERFORM_CHANGE_PASSWORD);
		opList.add(PERFORM_RETRIEVE_PASSWORD);
		opList.addAll(_adminOpList);
		_userOpList = opList;
	}

	//Other operations
        protected static final String REGISTER = "Register"; // displays the register page
        protected static final String PERFORM_REGISTER = "PerformRegister"; // performs the registration action
	protected static final String LOGIN = "Login";
        protected static final String BLANK = "Info"; // a dummy page just for showing an error message

  // Authentication config settings can disable some operations
  protected ArrayList<String> _disabledOpList;
	//the services on offer
	public static final String AUTHENTICATION_SERVICE = "Authentication";
	public static final String GET_USER_INFORMATION_SERVICE = "GetUserInformation";
	protected static final String CHANGE_USER_EDIT_MODE_SERVICE = "ChangeUserEditMode";
	protected static final String REMOTE_AUTHENTICATION_SERVICE = "RemoteAuthentication";

	protected static boolean _derbyWrapperDoneForcedShutdown = false;
  
  // some XML strings
      protected static final String RECAPTCHA_ELEM = "recaptcha";
  protected static final String OPERATIONS = "operations";
  protected static final String OPERATION = "operation";
  protected static final String USERNAME = "username";
  protected static final String PASSWORD = "password";
  protected static final String COLLECTION = "collection";
  protected static final String GROUPS = "groups";
  protected static final String COMPACTED_GROUPS = "compacted_groups";
  protected static final String EXPANDED_GROUPS = "expanded_groups";
  protected static final String STATUS = "status";
  protected static final String RECAPTCHA_KEY = "recaptcha_key";
  protected static final String COMMENT = "comment";
  protected static final String EMAIL = "email";
  
  // cgi params
  protected static final String USERNAME_PARAM = "username";
  protected static final String PREV_USERNAME_PARAM = "prevUsername";
  protected static final String NEW_USERNAME_PARAM = "newUsername";
  protected static final String PASSWORD_PARAM = "password";
  protected static final String OLD_PASSWORD_PARAM = "oldPassword";
  protected static final String NEW_PASSWORD_PARAM = "newPassword";
  protected static final String GROUPS_PARAM = "groups";
  protected static final String ENABLED_PARAM = "enabled";
  protected static final String COMMENT_PARAM = "comment";
  protected static final String STATUS_PARAM = "status";
  protected static final String EMAIL_PARAM = "email";
  protected static final String NEW_EMAIL_PARAM = "newEmail";
  protected static final String ACCOUNT_STATUS_PARAM = "accountstatus";
  protected static final String EDIT_ENABLED_PARAM = "editEnabled";
  public static final String AUTHPAGE_PARAM = "authpage";
  public static final String RECAPTCHA_RESPONSE_PARAM = "g-recaptcha-response";
  
  protected String _recaptchaSiteKey = null;
  protected static ArrayList<String> _recaptchaOpList = null;

  protected boolean selfRegisterEnabled = true;
  protected boolean userManagementEnabled = true;
  protected boolean accountSettingsEnabled = true;
  
  /** constructor */
	public Authentication()
	{
	}

	public void cleanUp()
	{
		super.cleanUp();

		if (!_derbyWrapperDoneForcedShutdown)
		{

			// This boolean is used to ensure we always shutdown the derby server, even if it is never
			// used by the Authentication server.  This is because the Tomcat greenstone3.xml
			// config file also specifies a connection to the database, which can result in the
			// server being initialized when the servlet is first accessed.  Note also, 
			// Authentication is a ServiceRack, meaning cleanUp() is called for each service
			// supported, however we only need to shutdown the Derby server once.  Again
			// this boolean variable helps achieve this.

			logger.info("Authentication Service performing forced shutdown of Derby Server ...");

			DerbyWrapper.shutdownDatabaseServer();
			_derbyWrapperDoneForcedShutdown = true;
		}
	}

	public boolean configure(Element info, Element extra_info)
	{
		logger.info("Configuring Authentication...");
		this.config_info = info;
                this._disabledOpList = new ArrayList<String>();

		Element getUserInformation_service = this.desc_doc.createElement(GSXML.SERVICE_ELEM);
		getUserInformation_service.setAttribute(GSXML.TYPE_ATT, GSXML.SERVICE_TYPE_PROCESS);
		getUserInformation_service.setAttribute(GSXML.NAME_ATT, GET_USER_INFORMATION_SERVICE);
		this.short_service_info.appendChild(getUserInformation_service);

		Element changeEditMode_service = this.desc_doc.createElement(GSXML.SERVICE_ELEM);
		changeEditMode_service.setAttribute(GSXML.TYPE_ATT, GSXML.SERVICE_TYPE_PROCESS);
		changeEditMode_service.setAttribute(GSXML.NAME_ATT, CHANGE_USER_EDIT_MODE_SERVICE);
		this.short_service_info.appendChild(changeEditMode_service);
		
		Element remoteAuthentication_service = this.desc_doc.createElement(GSXML.SERVICE_ELEM);
		remoteAuthentication_service.setAttribute(GSXML.TYPE_ATT, GSXML.SERVICE_TYPE_PROCESS);
		remoteAuthentication_service.setAttribute(GSXML.NAME_ATT, REMOTE_AUTHENTICATION_SERVICE);
		this.short_service_info.appendChild(remoteAuthentication_service);

                // this is called to set up the usersDB in case an existing one (web/etc/usersDB) has
                // been deleted
 		// a sample db is added on greenstone install, but it can be deleted to clear users

		DerbyWrapper.createDatabaseIfNeeded();

                
                // process the options
                NodeList options = info.getElementsByTagName(GSXML.OPTION_ELEM);
                for (int i = 0; i < options.getLength(); i++) {
                  Element option = (Element) options.item(i);
                  String option_name = option.getAttribute(GSXML.NAME_ATT);
                  String option_value = option.getAttribute(GSXML.VALUE_ATT);

                  if (option_name.equals("SelfRegister") && option_value.equals("false")) {
                    selfRegisterEnabled = false;
                      _disabledOpList.add(REGISTER);
                      _disabledOpList.add(PERFORM_REGISTER);
                  }
                  else if (option_name.equals("UserManagement") && option_value.equals("false")) {
                    userManagementEnabled = false;
                    _disabledOpList.add(LIST_USERS);
                    _disabledOpList.add(PERFORM_ADD);
                    _disabledOpList.add(PERFORM_EDIT);
                    _disabledOpList.add(ADD_USER);
                    _disabledOpList.add(EDIT_USER);
                    _disabledOpList.add(PERFORM_DELETE_USER);
                  }
                  else if (option_name.equals("SelfAccountSettingsManagement") && option_value.equals("false")) {
                    accountSettingsEnabled = false;
                    _disabledOpList.add(ACCOUNT_SETTINGS);
                    _disabledOpList.add(PERFORM_ACCOUNT_EDIT);
                    _disabledOpList.add(PERFORM_RESET_PASSWORD);
                    _disabledOpList.add(PERFORM_CHANGE_PASSWORD);
                    _disabledOpList.add(PERFORM_RETRIEVE_PASSWORD);
                  }
                  else if (option_name.equals("RecaptchaOperations")) {
                    if (RecaptchaUtil.hasKeys()) {
                      
                      _recaptchaSiteKey = RecaptchaUtil.getSiteKey();
                      
                      _recaptchaOpList = new ArrayList<String>();
                      String[] ops = option_value.split(",");
                      for (int j=0; j<ops.length; j++) {
                        if (!ops[j].equals("")) {
                          _recaptchaOpList.add(ops[j]); /// value checking?
                        }
                      }
                    }
                    else {
                      _recaptchaSiteKey = null;
                      _recaptchaOpList = null;
                    }
                  }
                  
		}
		// while all of our params are "not saved" for the session, a few of them are also sensitive, so should not be listed in the page response XML
		this.sensitive_params.add(PASSWORD_PARAM);
		this.sensitive_params.add(NEW_PASSWORD_PARAM);
		this.sensitive_params.add(OLD_PASSWORD_PARAM);
		this.sensitive_params.add(RECAPTCHA_RESPONSE_PARAM);

		// set up Authentication service info - for now just has name and type
		Element authentication_service = this.desc_doc.createElement(GSXML.SERVICE_ELEM);
		authentication_service.setAttribute(GSXML.TYPE_ATT, "authen");
		authentication_service.setAttribute(GSXML.NAME_ATT, AUTHENTICATION_SERVICE);
                authentication_service.setAttribute("selfRegister", (selfRegisterEnabled ? "true": "false"));
                authentication_service.setAttribute("userManagement", (userManagementEnabled? "true": "false"));
                authentication_service.setAttribute("selfAccountSettingsManagement", (accountSettingsEnabled? "true": "false"));
		this.short_service_info.appendChild(authentication_service);

		
		return true;
	}

  protected Element getServiceDescription(Document doc, String service_id, String lang, String subset)
	{

		Element authen_service = doc.createElement(GSXML.SERVICE_ELEM);

		if (service_id.equals(AUTHENTICATION_SERVICE) )
		{
			authen_service.setAttribute(GSXML.TYPE_ATT, "authen");
			authen_service.setAttribute(GSXML.NAME_ATT, AUTHENTICATION_SERVICE);
                        if (subset == null || subset.equals(GSXML.DISPLAY_TEXT_ELEM + GSXML.LIST_MODIFIER)) {
                          // add in the displayInfo
                          authen_service.appendChild(GSXML.createDisplayTextElement(doc, GSXML.DISPLAY_TEXT_NAME, getServiceDisplayName(service_id, lang)));
                          authen_service.appendChild(GSXML.createDisplayTextElement(doc, GSXML.DISPLAY_TEXT_DESCRIPTION, getServiceDisplayDescription(service_id, lang)));
                          
                        }
                        authen_service.setAttribute("selfRegister", (selfRegisterEnabled ? "true": "false"));
                        authen_service.setAttribute("userManagement", (userManagementEnabled? "true": "false"));
                        authen_service.setAttribute("selfAccountSettingsManagement", (accountSettingsEnabled? "true": "false"));
                        
		}
		else if (service_id.equals(GET_USER_INFORMATION_SERVICE))
		{
			authen_service.setAttribute(GSXML.TYPE_ATT, GSXML.SERVICE_TYPE_PROCESS);
			authen_service.setAttribute(GSXML.NAME_ATT, GET_USER_INFORMATION_SERVICE);
		}
		else if (service_id.equals(CHANGE_USER_EDIT_MODE_SERVICE))
		{
			authen_service.setAttribute(GSXML.TYPE_ATT, GSXML.SERVICE_TYPE_PROCESS);
			authen_service.setAttribute(GSXML.NAME_ATT, CHANGE_USER_EDIT_MODE_SERVICE);
		}
		else if (service_id.equals(REMOTE_AUTHENTICATION_SERVICE))
		{
			authen_service.setAttribute(GSXML.TYPE_ATT, GSXML.SERVICE_TYPE_PROCESS);
			authen_service.setAttribute(GSXML.NAME_ATT, REMOTE_AUTHENTICATION_SERVICE);
		}		
		else
		{
			return null;
		}
		return authen_service;
	}

	protected String getServiceDisplayName(String service_id, String lang)
	{
		return getTextString(service_id + ".name", lang);
	}

	protected String getServiceDisplaySubmit(String service_id, String lang)
	{
		return getTextString(service_id + ".submit", lang);
	}

	protected String getServiceDisplayDescription(String service_id, String lang)
	{
		return getTextString(service_id + ".description", lang);
	}
  	protected String getErrorTextString(int error_code, String lang) {
	  return getTextString(_errorKeyMap.get(error_code), lang);
	  
	}
  public static String getErrorKey(int error_code) {
    
    return _errorKeyMap.get(error_code);
  }
	protected Element processChangeUserEditMode(Element request)
	{
		// Create a new (empty) result message
	  Document result_doc = XMLConverter.newDOM();
		Element result = result_doc.createElement(GSXML.RESPONSE_ELEM);

		result.setAttribute(GSXML.FROM_ATT, CHANGE_USER_EDIT_MODE_SERVICE);
		result.setAttribute(GSXML.TYPE_ATT, GSXML.REQUEST_TYPE_PROCESS);

		Element paramList = (Element) GSXML.getChildByTagName(request, GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
		if (paramList == null)
		{
		  logger.error("ChangeUserEditMode request has no param list!!");
		  return result;
		}

		HashMap<String, Serializable> params = GSXML.extractParams(paramList, true);

		String username = (String) params.get(USERNAME_PARAM);
		String editMode = (String) params.get(ENABLED_PARAM);

		if (!editMode.toLowerCase().equals("true") && !editMode.toLowerCase().equals("false"))
		{
			editMode = "false";
		}

		DerbyWrapper dw = openDatabase();
		dw.addUserData(username, "USER_EDIT_ENABLED", editMode);
		dw.closeDatabase();

		return result;
	}

	/**
	 * This method replaces the gliserver.pl code for authenticating a user against the derby database
	 * gliserver.pl needed to instantiate its own JVM to access the derby DB, but the GS3 already has
	 * the Derby DB open and 2 JVMs are not allowed concurrent access to an open embedded Derby DB.
	 * Gliserver.pl now goes through this method (via ServletRealmCheck.java), thereby using the same 
	 * connection to the DerbyDB. This method reproduces the same behaviour as gliserver.pl used to,
	 * by returning the user_groups on successful authentication, else returns the specific 
	 * "Authentication failed" messages that gliserver.pl would produce.
	 * http://remote-host-name:8383/greenstone3/library?a=s&sa=authenticated-ping&excerptid=gs_content&un=admin&pw=<PW>&col=demo
	*/
	protected Element processRemoteAuthentication(Element request) {
		//logger.info("*** Authentication::processRemoteAuthentication");	
		
		String message = "";
		
		Element system = (Element) GSXML.getChildByTagName(request, GSXML.REQUEST_TYPE_SYSTEM);		
		String username = system.hasAttribute(USERNAME) ? system.getAttribute(USERNAME) : "";
		String password = system.hasAttribute(PASSWORD) ? system.getAttribute(PASSWORD) : "";
		
		
		// If we're not editing a collection then the user doesn't need to be in a particular group
		String collection = system.hasAttribute(COLLECTION) ? system.getAttribute(COLLECTION) : "";
				
		
		if(username.equals("") || password.equals("")) {
			message = "Authentication failed: no (username or) password specified.";
			//logger.error("*** Remote login failed. No username or pwd provided");
		}		
		else {		
			String storedPassword = retrieveDataForUser(username, PASSWORD);
			if(storedPassword != null && (password.equals(storedPassword) || AuthenticationHelper.hashPassword(password).equals(storedPassword))) {
				
				// gliserver.pl used to return the groups when authentication succeeded
				String groups = retrieveDataForUser(username, EXPANDED_GROUPS); //comma-separated list
				
				if(collection.equals("")) {
                                  // if no collection specified, just return all groups
					message = groups;
				} else { 					
                                  // if we have a collection specified, only return the group list if the user can edit this collection
                                  if (GroupsUtil.canEditCollection(username, groups, collection)) {
                                    message = groups;
                                  } 
                                  else {
                                    message = "Authentication failed: user is not in the required group.";
                                    //logger.error("*** Remote login failed. Groups did not match for the collection specified");
                                  }
				}
				
			} else {
				
				if(storedPassword == null) {
					message = "Authentication failed: no account for user '" + username + "'";
					//logger.error("*** Remote login failed. User not found or password not set for user.");
				} else {
					message = "Authentication failed: incorrect password.";
					//logger.error("*** Remote login failed. Password did not match for user");
				}
			}
		}
		Document result_doc = XMLConverter.newDOM();
		Element result = result_doc.createElement(GSXML.RESPONSE_ELEM);
		result.setAttribute(GSXML.FROM_ATT, REMOTE_AUTHENTICATION_SERVICE);
		result.setAttribute(GSXML.TYPE_ATT, GSXML.REQUEST_TYPE_PROCESS);		
		Element s = GSXML.createTextElement(result_doc, GSXML.STATUS_ELEM, message);
		result.appendChild(s);
		return result;
	}
	
	protected Element processGetUserInformation(Element request)
	{
		// Create a new (empty) result message
	  Document result_doc = XMLConverter.newDOM();
		Element result = result_doc.createElement(GSXML.RESPONSE_ELEM);

		result.setAttribute(GSXML.FROM_ATT, GET_USER_INFORMATION_SERVICE);
		result.setAttribute(GSXML.TYPE_ATT, GSXML.REQUEST_TYPE_PROCESS);

		String lang = request.getAttribute(GSXML.LANG_ATT);
		Element paramList = (Element) GSXML.getChildByTagName(request, GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
		if (paramList == null)
		{
		  logger.error("GetUserInformation request has no param list");
		  return result;
		}

		HashMap<String, Serializable> params = GSXML.extractParams(paramList, true);

		String username = (String) params.get(USERNAME_PARAM);

		if (username == null)
		{
			GSXML.addError(result, getErrorTextString(ERROR_USERNAME_NOT_SPECIFIED, lang));
			return result;
		}

		DerbyWrapper derbyWrapper = openDatabase();

		UserQueryResult userQueryResult = derbyWrapper.findUser(username);
		String editEnabled = derbyWrapper.getUserData(username, "USER_EDIT_ENABLED");

		Vector<UserTermInfo> terms = userQueryResult.getUserTerms();

		if (terms.size() == 0)
		{
			GSXML.addError(result, getErrorTextString(ERROR_USER_NOT_FOUND, lang));
			return result;
		}

		UserTermInfo userInfo = terms.get(0);
		Element userInfoList = result_doc.createElement(GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
		result.appendChild(userInfoList);

		Element usernameField = GSXML.createParameter(result_doc, USERNAME_PARAM, userInfo.getUsername());
		Element passwordField = GSXML.createParameter(result_doc, PASSWORD_PARAM, userInfo.getPassword());
		Element groupsField = GSXML.createParameter(result_doc, GROUPS_PARAM, userInfo.getExpandedGroups()); // used by LibraryServlet that calls the containing method
		Element accountStatusField = GSXML.createParameter(result_doc, ACCOUNT_STATUS_PARAM, userInfo.getAccountStatus());
		Element commentField = GSXML.createParameter(result_doc, COMMENT_PARAM, userInfo.getComment());

		if (editEnabled != null)
		{
			Element editEnabledElem = GSXML.createParameter(result_doc, EDIT_ENABLED_PARAM, editEnabled);
			userInfoList.appendChild(editEnabledElem);
		}

		userInfoList.appendChild(usernameField);
		userInfoList.appendChild(passwordField);
		userInfoList.appendChild(groupsField);
		userInfoList.appendChild(accountStatusField);
		userInfoList.appendChild(commentField);

		derbyWrapper.closeDatabase();

		return result;
	}

	protected Element processAuthentication(Element request)
	{
		checkAdminUserExists();

		// Create a new (empty) result message
		Document result_doc = XMLConverter.newDOM();
		Element result = result_doc.createElement(GSXML.RESPONSE_ELEM);
		result.setAttribute(GSXML.FROM_ATT, AUTHENTICATION_SERVICE);
		result.setAttribute(GSXML.TYPE_ATT, GSXML.REQUEST_TYPE_PROCESS);

		// Create an Authentication node put into the result
		Element authenNode = result_doc.createElement(GSXML.AUTHEN_NODE_ELEM);
		result.appendChild(authenNode);
                // why??
                result.appendChild(getCollectList(result_doc, this.site_home + File.separatorChar + "collect"));

		// Create a service node added into the Authentication node
		Element serviceNode = result_doc.createElement(GSXML.SERVICE_ELEM);
		authenNode.appendChild(serviceNode);

		// Get the parameters of the request
		String lang = request.getAttribute(GSXML.LANG_ATT);
		Element param_list = (Element) GSXML.getChildByTagName(request, GSXML.PARAM_ELEM + GSXML.LIST_MODIFIER);
		if (param_list == null)
		{
			serviceNode.setAttribute(OPERATION, BLANK);
			logger.error("Authentication request has no param list");
			return result; // Return the empty result
		}
		HashMap<String, Serializable> paramMap = GSXML.extractParams(param_list, false);
		String op = (String) paramMap.get(AUTHPAGE_PARAM);
		serviceNode.setAttribute(OPERATION, op);
                if (_disabledOpList.contains(op)) {
                  return result; // empty
                }
		String username = null;
		String groups = null;

		Element userInformation = (Element) GSXML.getChildByTagName(request, GSXML.USER_INFORMATION_ELEM);
		if (userInformation != null)
		{
			username = userInformation.getAttribute(GSXML.USERNAME_ATT);
			groups = userInformation.getAttribute(GSXML.GROUPS_ATT);
		}
		logger.debug("username="+username+", groups = "+groups);
		logger.debug("expandedGroups would be: "+UserTermInfo.expandGroups(groups));
		logger.debug("compactedGroups would be: "+UserTermInfo.compactGroups(groups));
		
		if ((userInformation == null || username == null) && _userOpList.contains(op))
		{
		  // its an operation that requires the user to be logged on - direct them to login page
			serviceNode.setAttribute(OPERATION, LOGIN);
			if (_recaptchaOpList != null && _recaptchaOpList.contains(LOGIN)) {
			  serviceNode.setAttribute(RECAPTCHA_KEY, _recaptchaSiteKey);
			}
			GSXML.addError(result, getErrorTextString(ERROR_NOT_LOGGED_IN, lang));
			return result;
		}

		if (_adminOpList.contains(op) && (groups == null || !groups.matches(".*\\badministrator\\b.*")))
		{
		  // actually, the user needs to be an admin user and they are not
			serviceNode.setAttribute(OPERATION, LOGIN);
			if (_recaptchaOpList != null && _recaptchaOpList.contains(LOGIN)) {
			  serviceNode.setAttribute(RECAPTCHA_KEY, _recaptchaSiteKey);
			}
			GSXML.addError(result, getErrorTextString(ERROR_ADMIN_NOT_LOGGED_IN, lang));
			return result;
		}

		if (_recaptchaOpList != null && _recaptchaOpList.contains(op)) {
		  serviceNode.setAttribute(RECAPTCHA_KEY, _recaptchaSiteKey);
		}
		
		if (op.equals(LIST_USERS))
		{
		  int error = addUserInformationToNode(null, serviceNode);
		  if (error != NO_ERROR)
		    {
		      serviceNode.setAttribute(OPERATION, BLANK);
		      GSXML.addError(result, getErrorTextString(error, lang));
		    }
		  return result;
		  
		}
				
		if (op.equals(PERFORM_ADD))
		{
			String newUsername = (String) paramMap.get(USERNAME_PARAM);
			String newPassword = (String) paramMap.get(PASSWORD_PARAM);
			String newGroups = (String) paramMap.get(GROUPS_PARAM);
			String newStatus = (String) paramMap.get(STATUS_PARAM);
			String newComment = (String) paramMap.get(COMMENT_PARAM);
			String newEmail = (String) paramMap.get(EMAIL_PARAM);

			if (_recaptchaOpList != null && _recaptchaOpList.contains(ADD_USER)) {	  
			  serviceNode.setAttribute(RECAPTCHA_KEY, _recaptchaSiteKey);
			}
			//Check the given user name
			int error;
			if (checkUserExists(newUsername)) {
			  error = ERROR_USER_ALREADY_EXISTS;
			} else {
			  error = checkUsername(newUsername);
			}
			if (error != NO_ERROR)
			{
			  serviceNode.setAttribute(OPERATION, ADD_USER);
			  GSXML.addError(result, getErrorTextString(error, lang));
			  return result;
			}

			//Check the given password
			if ((error = checkPassword(newPassword)) != NO_ERROR)
			{
			  serviceNode.setAttribute(OPERATION, ADD_USER);
				GSXML.addError(result, getErrorTextString(error, lang));
				return result;
			}

			newPassword = AuthenticationHelper.hashPassword(newPassword);

			error = addUser(newUsername, newPassword, newGroups, newStatus, newComment, newEmail);
			if (error != NO_ERROR)
			{
				serviceNode.setAttribute(OPERATION, ADD_USER);
				GSXML.addError(result, getErrorTextString(error, lang));
			}
			else
			{
				addUserInformationToNode(null, serviceNode);
				serviceNode.setAttribute(OPERATION, LIST_USERS);
			}
			return result;
		}

		if (op.equals(REGISTER)) {
		  // don't need any additional info
		  return result;
		}
		if (op.equals(PERFORM_REGISTER))
		{
			String newUsername = (String) paramMap.get(USERNAME_PARAM);
			String newPassword = (String) paramMap.get(PASSWORD_PARAM);
			String newEmail = (String) paramMap.get(EMAIL_PARAM);

			//Check the given details
			int error;
			if (checkUserExists(newUsername)) {
			  error = ERROR_USER_ALREADY_EXISTS;
			} else {
			  error = checkUsername(newUsername);
			}
			if (error == NO_ERROR) {
			  if ((error = checkPassword(newPassword)) == NO_ERROR) {
			    newPassword = AuthenticationHelper.hashPassword(newPassword);
			    if (_recaptchaOpList != null && _recaptchaOpList.contains(REGISTER)) {
			      String user_response = (String) paramMap.get(RECAPTCHA_RESPONSE_PARAM);
			      if ((error = RecaptchaUtil.verifyRecaptcha(user_response)) == RecaptchaUtil.NO_ERROR) {
				error = addUser(newUsername, newPassword, "", "true", "", newEmail);
			      }
			    }
			  }
			}
			
			if (error != NO_ERROR)
			{
				serviceNode.setAttribute(OPERATION, REGISTER);
				if (_recaptchaOpList != null && _recaptchaOpList.contains(REGISTER)) {
				  serviceNode.setAttribute(RECAPTCHA_KEY, _recaptchaSiteKey);
				}
				GSXML.addError(result, getErrorTextString(error, lang));
			}
			// otherwise everything hunky dory and we return result
			return result;
		}

		// PERFORM_EDIT is caled when admin is running EditUser, and PERFORM_ACCOUNT_EDIT is called when a user is running AccountSettings to change their own details
		if (op.equals(PERFORM_EDIT) || op.equals(PERFORM_ACCOUNT_EDIT)) {
		  
		  String parent_op = EDIT_USER;
		  if (op.equals(PERFORM_ACCOUNT_EDIT)) {
		    parent_op = ACCOUNT_SETTINGS;
		  }
		  String previousUsername = (String) paramMap.get(PREV_USERNAME_PARAM);
		  String newUsername = (String) paramMap.get(NEW_USERNAME_PARAM);
		  int error;
		  // Has the user name been changed? Make sure it doesn't already exist and is a valid username
		  if (previousUsername == null) {
		    // we have ended up here by mistake (via s1.authpage which is no longer valid)
		    serviceNode.setAttribute(OPERATION, BLANK);
		    GSXML.addError(result, getErrorTextString(ERROR_SOMETHING_WRONG, lang));
		    return result;
		  }
		    
		  if (!previousUsername.equals(newUsername)) {

		    error = NO_ERROR;
		    if (checkUserExists(newUsername)) {
		      error = ERROR_USER_ALREADY_EXISTS;
		    } else {
		      error = checkUsername(newUsername);
		    }
		    if (error != NO_ERROR) {
		      addUserInformationToNode(previousUsername, serviceNode);
		      serviceNode.setAttribute(OPERATION, parent_op);
		      GSXML.addError(result, getErrorTextString(error, lang));
		      return result;
		    }
		  }

		  // password checking
		  String newPassword;
		  if (op.equals(PERFORM_EDIT)) {
		    newPassword = (String) paramMap.get(PASSWORD_PARAM);
		  } else {
		    newPassword = (String) paramMap.get(NEW_PASSWORD_PARAM);
		  }
		  if (newPassword == null) {
		    // we are not changing the password
		    newPassword = retrieveDataForUser(previousUsername, PASSWORD);
		  } else {
		    // we need to check the old one
		    if (op.equals(PERFORM_ACCOUNT_EDIT)) {
		      // check that they entered their old password correctly
		      String prevPassword = retrieveDataForUser(previousUsername, PASSWORD);
		      String oldPassword = (String) paramMap.get(OLD_PASSWORD_PARAM);
		      oldPassword = AuthenticationHelper.hashPassword(oldPassword);
		      if (oldPassword == null || !oldPassword.equals(prevPassword)) {
			
			addUserInformationToNode(previousUsername, serviceNode);
			serviceNode.setAttribute(OPERATION, ACCOUNT_SETTINGS);
			GSXML.addError(result, getErrorTextString(ERROR_INCORRECT_PASSWORD, lang), "INCORRECT_PASSWORD");
			return result;
		      }
		    }
		    // need to make sure the new password is a valid password
		    if ((error = checkPassword(newPassword)) != NO_ERROR) {
		      
		      addUserInformationToNode(previousUsername, serviceNode);
		      serviceNode.setAttribute(OPERATION, parent_op);
		      GSXML.addError(result, getErrorTextString(error, lang));
		      return result;
		    }
		    newPassword = AuthenticationHelper.hashPassword(newPassword);
		  
		  }

		  // are we using recaptcha for AccountSettings or EditUser?
		  if (_recaptchaOpList != null && _recaptchaOpList.contains(parent_op)) {
		    String user_response = (String) paramMap.get(RECAPTCHA_RESPONSE_PARAM);
		    if ((error= RecaptchaUtil.verifyRecaptcha(user_response)) != NO_ERROR) {
		      addUserInformationToNode(previousUsername, serviceNode);
		      serviceNode.setAttribute(OPERATION, parent_op);
		      serviceNode.setAttribute(RECAPTCHA_KEY, _recaptchaSiteKey);
		      GSXML.addError(result, getErrorTextString(error, lang));
		      return result;
		    }
		  }
		  
		  groups = null; // doesn't matter if local groups var keeps value in compactedGroups or expandedGroups form,
		  // as addUser() and addUserInformationToNode() will ensure use of expanded and compacted forms respectively
		  String status = null;
		  String comment = null;
		  String email = (String) paramMap.get(NEW_EMAIL_PARAM);
		  if (op.equals(PERFORM_EDIT)) {
			groups = (String) paramMap.get(GROUPS_PARAM);
			status = (String) paramMap.get(STATUS_PARAM);
			comment = (String) paramMap.get(COMMENT_PARAM);

		  } else {
		    groups = retrieveDataForUser(previousUsername, EXPANDED_GROUPS);
		    status = retrieveDataForUser(previousUsername, STATUS);
		    comment = retrieveDataForUser(previousUsername, COMMENT);
		  }
		  

		  error = removeUser(previousUsername);
		  if (error != NO_ERROR) {
		    addUserInformationToNode(previousUsername, serviceNode);
		    serviceNode.setAttribute(OPERATION, parent_op);
		    GSXML.addError(result, getErrorTextString(error, lang));
		    return result;
		  }
		  
		  error = addUser(newUsername, newPassword, groups, status, comment, email);
		  if (error != NO_ERROR) {
		    
		    // oh dear. we have removed previous data, but were unable to add new data. The user is now gone :-(
		    serviceNode.setAttribute(OPERATION, BLANK);
		    GSXML.addError(result, getErrorTextString(error, lang));
		  }
		  else {
		    if (op.equals(PERFORM_ACCOUNT_EDIT)) {
		      addUserInformationToNode(newUsername, serviceNode);
		      serviceNode.setAttribute(OPERATION, parent_op);
		      if (_recaptchaOpList != null && _recaptchaOpList.contains(parent_op)) {
			serviceNode.setAttribute(RECAPTCHA_KEY, _recaptchaSiteKey);
		      }
		      GSXML.addError(result, getTextString("auth.success.account_settings", lang));

		    } else {
		      
		      addUserInformationToNode(null, serviceNode);
		      serviceNode.setAttribute(OPERATION, LIST_USERS);
		      String [] args = {newUsername};
		      GSXML.addError(result, getTextString("auth.success.edit_user", lang, args));
		    }
		    
		  }
		  return result;
		}
		if (op.equals(PERFORM_RETRIEVE_PASSWORD))
		{
		  return result;
		}
		if (op.equals(PERFORM_CHANGE_PASSWORD))
		{
			serviceNode.setAttribute(OPERATION, PERFORM_CHANGE_PASSWORD);
			String user_name = (String) paramMap.get(USERNAME_PARAM);
			String oldPassword = (String) paramMap.get(OLD_PASSWORD_PARAM);
			String newPassword = (String) paramMap.get(NEW_PASSWORD_PARAM);
			if (user_name == null || oldPassword == null || newPassword == null)
			{
				GSXML.addError(result, getErrorTextString(ERROR_MISSING_PARAMS, lang));
				return result;
			}

			String prevPassword = retrieveDataForUser(user_name, PASSWORD);
			if (!AuthenticationHelper.hashPassword(oldPassword).equals(prevPassword))
			{
				addUserInformationToNode(user_name, serviceNode);
				GSXML.addError(result, getErrorTextString(ERROR_INCORRECT_PASSWORD, lang), "INCORRECT_PASSWORD");
				return result;
			}

			//Check the given password
			int error;
			if ((error = checkPassword(newPassword)) != NO_ERROR)
			{
				GSXML.addError(result, getErrorTextString(error, lang));
				return result;
			}

			DerbyWrapper derbyWrapper = openDatabase();
			String chpa_groups = retrieveDataForUser(user_name, EXPANDED_GROUPS); // userDB now stores expandedGroups, not compactedGroups
			String chpa_comment = "password_changed_by_user";
			String info = derbyWrapper.modifyUserInfo(user_name, AuthenticationHelper.hashPassword(newPassword), chpa_groups, null, chpa_comment, null);
			derbyWrapper.closeDatabase();
			if (info != "succeed")
			{//see DerbyWrapper.modifyUserInfo
			  GSXML.addError(result, info); 
				return result;
			}
			return result;
		}
		if (op.equals(EDIT_USER))
		{
			String editUsername = (String) paramMap.get(USERNAME_PARAM);
			int error = addUserInformationToNode(editUsername, serviceNode);
			if (error != NO_ERROR)
			{
			  GSXML.addError(result, getErrorTextString(error, lang));
			}
			return result;
		}
		if (op.equals(ACCOUNT_SETTINGS))
		{
			String editUsername = (String) paramMap.get(USERNAME_PARAM);

			if (editUsername == null)
			{
				serviceNode.setAttribute(OPERATION, "");
				GSXML.addError(result, getErrorTextString(ERROR_USERNAME_NOT_SPECIFIED, lang));
				return result;
			}

			if (!editUsername.equals(username))
			{
				serviceNode.setAttribute(OPERATION, LOGIN);
				GSXML.addError(result, getErrorTextString(ERROR_USER_NOT_AUTHORISED, lang));
				return result;
			}
			int error = addUserInformationToNode(editUsername, serviceNode);
			if (error != NO_ERROR)
			{
			  GSXML.addError(result, getErrorTextString(error, lang));
			}
			return result;
		}
		if (op.equals(PERFORM_RESET_PASSWORD))
		{
			String passwordResetUser = (String) paramMap.get(USERNAME_PARAM);

			String newPassword = UUID.randomUUID().toString();
			newPassword = newPassword.substring(0, newPassword.indexOf("-"));

			String email = retrieveDataForUser(passwordResetUser, EMAIL);
			String from = "admin@greenstone.org";
			String host = request.getAttribute("remoteAddress");

			//TODO: FINISH THIS
			return result;
		}
		if (op.equals(PERFORM_DELETE_USER))
		{
			String usernameToDelete = (String) paramMap.get(USERNAME_PARAM);
			int error = removeUser(usernameToDelete);
			if (error != NO_ERROR)
			{
			  GSXML.addError(result, getErrorTextString(error, lang));
			}
			addUserInformationToNode(null, serviceNode);
			serviceNode.setAttribute(OPERATION, LIST_USERS);
			String[] args = {usernameToDelete};
			GSXML.addError(result, getTextString("auth.success.delete_user", lang, args));
			return result;
		}

		return result; // or should we return null, as we haven't recognised the operation??
		}

	public int checkUsernameAndPassword(String username, String password)
	{
		int uResult = checkUsername(username);
		int pResult = checkPassword(password);

		return (uResult != NO_ERROR ? uResult : (pResult != NO_ERROR ? pResult : NO_ERROR));
	}

	public int checkUsername(String username)
	{
		//Check the given user name
		if ((username == null) || (username.length() < USERNAME_MIN_LENGTH) || (username.length() > USERNAME_MAX_LENGTH) || (!(Pattern.matches("[a-zA-Z0-9//_//.]+", username))))
		{
			return ERROR_INVALID_USERNAME;
		}
		return NO_ERROR;
	}

	public int checkPassword(String password)
	{
		//Check the given password
		if (password == null)
		{
			return ERROR_PASSWORD_NOT_ENTERED;
		}
		else if (password.length() < PASSWORD_MIN_LENGTH)
		{
			return ERROR_PASSWORD_TOO_SHORT;
		}
		else if (password.length() > PASSWORD_MAX_LENGTH)
		{
			return ERROR_PASSWORD_TOO_LONG;
		}
		else if (!(Pattern.matches("[\\p{ASCII}]+", password)))
		{
			return ERROR_PASSWORD_USES_ILLEGAL_CHARACTERS;
		}
		return NO_ERROR;
	}
	// This method can also be used for printing out the password in hex (in case
	// the password used the UTF-8 Charset), or the hex values in any unicode string.
	// From http://stackoverflow.com/questions/923863/converting-a-string-to-hexadecimal-in-java
	public static String toHex(String arg)
	{
		try
		{
			return String.format("%x", new BigInteger(arg.getBytes("US-ASCII"))); // set to same charset as used by AuthenticationHelper.hashPassword()
		}
		catch (Exception e)
		{ // UnsupportedEncodingException
			e.printStackTrace();
		}
		return "Unable to print";
	}

	private void checkAdminUserExists()
	{
		DerbyWrapper derbyWrapper = openDatabase();
		UserQueryResult userQueryResult = derbyWrapper.findUser(null, null);
		derbyWrapper.closeDatabase();

		if (userQueryResult != null)
		{
			Vector userInfo = userQueryResult.getUserTerms();

			boolean adminFound = false;
			for (int i = 0; i < userQueryResult.getSize(); i++)
			{
			    // can call either getExpandedGroups() or getCompactedGroups() to check admin group is in there:
			    if (((UserTermInfo) userInfo.get(i)).getCompactedGroups() != null && ((UserTermInfo) userInfo.get(i)).getCompactedGroups().matches(".*\\badministrator\\b.*"))
				{
					adminFound = true;
				}
			}

			if (!adminFound)
			{
				addUser("admin", "admin", "administrator", "true", "Change the password for this account as soon as possible", "");
			}
		}
	}

	private DerbyWrapper openDatabase()
	{
		// check the usersDb database, if it isn't existing, check the etc dir, create the etc dir if it isn't existing, then create the  user database and add a "admin" user
		String usersDB_dir = DerbyWrapper.USERSDB_DIR;
		DerbyWrapper derbyWrapper = new DerbyWrapper(usersDB_dir);
		return derbyWrapper;
	}

	private int addUserInformationToNode(String username, Element serviceNode)
	{
		DerbyWrapper derbyWrapper = openDatabase();
		UserQueryResult userQueryResult = derbyWrapper.findUser(username, null);
		derbyWrapper.closeDatabase();

		if (userQueryResult != null)
		{
		  Element user_node = getUserNodeList(serviceNode.getOwnerDocument(), userQueryResult);
			serviceNode.appendChild(user_node);
			return NO_ERROR;
		}

		return ERROR_COULD_NOT_GET_USER_INFO;
	}

	private int removeUser(String username)
	{
		if (username == null)
		{
			return ERROR_USERNAME_NOT_SPECIFIED;
		}

		DerbyWrapper derbyWrapper = openDatabase();
		boolean success = derbyWrapper.deleteUser(username);
		derbyWrapper.closeDatabase();

		if (success)
		{
			return NO_ERROR;
		}

		return ERROR_REMOVING_USER;
	}

	private int addUser(String newUsername, String newPassword, String newGroups, String newStatus, String newComment, String newEmail)
	{
		newGroups = newGroups.replaceAll(" ", "");
		// store only expandedGroups in DB now
		newGroups = UserTermInfo.expandGroups(newGroups);

		//Check if the user already exists
		DerbyWrapper derbyWrapper = openDatabase();
		UserQueryResult userQueryResult = derbyWrapper.findUser(newUsername, null);

		if (userQueryResult != null)
		{
			derbyWrapper.closeDatabase();
			return ERROR_USER_ALREADY_EXISTS;
		}
		else
		{
			boolean success = derbyWrapper.addUser(newUsername, newPassword, newGroups, newStatus, newComment, newEmail);
			derbyWrapper.closeDatabase();

			if (!success)
			{
				return ERROR_ADDING_USER;
			}
		}

		return NO_ERROR;
	}

	private boolean checkUserExists(String username)
	{
		boolean check_status = false;

		DerbyWrapper derbyWrapper = openDatabase();
		try
		{
			UserQueryResult result = derbyWrapper.findUser(username);

			if (result != null)
			{
				check_status = true;
			}

		}
		catch (Exception ex)
		{
			// some error occurred accessing the database
			ex.printStackTrace();
		}
		derbyWrapper.closeDatabase();

		return check_status;
	}

	private String retrieveDataForUser(String username, String dataType)
	{
		openDatabase();

		String data = null;

		try
		{
			DerbyWrapper derbyWrapper = openDatabase();
			UserQueryResult result = derbyWrapper.findUser(username);
			derbyWrapper.closeDatabase();
			if (result == null) {
			    return null;
			}
			Vector userInfo = result.getUserTerms();

			for (int i = 0; i < result.getSize(); i++)
			{
				if (dataType.equals(PASSWORD))
				{
				    data = ((UserTermInfo) userInfo.get(i)).getPassword();
					break;
				}
				else if (dataType.equals(COMPACTED_GROUPS))
				{
				    data = ((UserTermInfo) userInfo.get(i)).getCompactedGroups();
					break;
				}
				else if (dataType.equals(EXPANDED_GROUPS))
				{
				    data = ((UserTermInfo) userInfo.get(i)).getExpandedGroups();
				    break;
				}
				else if (dataType.equals(STATUS))
				{
				    data = ((UserTermInfo) userInfo.get(i)).getAccountStatus();
					break;
				}
				else if (dataType.equals(COMMENT))
				{
				    data = ((UserTermInfo) userInfo.get(i)).getComment();
					break;
				}
				else if (dataType.equals(EMAIL))
				{
				    data = ((UserTermInfo) userInfo.get(i)).getEmail();
					break;
				}
			}
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
		}

		return data;
	}

  private Element getUserNodeList(Document doc, UserQueryResult userQueryResult)
	{
		Element user_list_node = doc.createElement(GSXML.USER_NODE_ELEM + GSXML.LIST_MODIFIER);

		Vector userInfo = userQueryResult.getUserTerms();

		for (int i = 0; i < userQueryResult.getSize(); i++)
		{
			Element user_node = doc.createElement(GSXML.USER_NODE_ELEM);
			String username = ((UserTermInfo) userInfo.get(i)).getUsername();
			String compactedGroups = ((UserTermInfo) userInfo.get(i)).getCompactedGroups();
			// Since this is used for display in admin pages, get compacted (more similar to user-entered) groups
			// and not expanded groups, as getUserNodeList only used in addUserInformationToNode() which
			// allows displaying and modifying user info pages and authenticating admin user,
			// none of which requires expanded groups.
			String accountstatus = ((UserTermInfo) userInfo.get(i)).getAccountStatus();
			String comment = ((UserTermInfo) userInfo.get(i)).getComment();
			String email = ((UserTermInfo) userInfo.get(i)).getEmail();
			user_node.setAttribute(USERNAME, username);
			user_node.setAttribute(GROUPS, compactedGroups);
			user_node.setAttribute(STATUS, accountstatus);
			user_node.setAttribute(COMMENT, comment);
			user_node.setAttribute(EMAIL, email);

			user_list_node.appendChild(user_node);
		}
		return user_list_node;
	}

  private Element getCollectList(Document doc, String collect)
	{
		Element collect_list_node = doc.createElement(GSXML.COLLECTION_ELEM + GSXML.LIST_MODIFIER);
		File[] collect_dir = (new File(collect)).listFiles();
		if (collect_dir != null && collect_dir.length > 0)
		{
			for (int i = 0; i < collect_dir.length; i++)
			{
				if (collect_dir[i].isDirectory() && (!collect_dir[i].getName().startsWith(".svn")))
				{
					Element collect_node = doc.createElement(GSXML.COLLECTION_ELEM);
					collect_node.setAttribute(GSXML.NAME_ATT, collect_dir[i].getName());
					collect_list_node.appendChild(collect_node);
				}
			}
		}
		return collect_list_node;
	}


	// main() method - calls hashPassword() on any String argument, printing this to stdout
	// This main() is invoked by gliserver.pl perl code to encrypt passwords identically to Java code.
        //
        // !!!!
        // **** With the function moving to util.AuthenticationHeloper, would probably make more sense now
        // **** to move main() to this util class also, and update gliserver.pl
        // !!!!
	public static void main(String[] args)
	{
		if (args.length < 1)
		{
			System.err.println("Usage: Authentication <string to encrypt>");
			System.exit(-1);
		}
		// just hash the first argument
		String hash = AuthenticationHelper.hashPassword(args[0]);
		System.out.println(hash);
	}
}
