/***********************************************************************
 *
 *  Copyright (C) 2006 Novell, Inc. All Rights Reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; version 2.1
 *  of the License.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Library Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, Novell, Inc.
 *
 *  To contact Novell about this file by physical or electronic mail,
 *  you may find current contact information at www.novell.com.
 *
 *  Author: Juan Carlos Luciani <jluciani@novell.com>
 *
 ***********************************************************************/

package com.novell.casa.authtoksvc;

import org.bandit.util.config.gen.*;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.ServiceUnavailableException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.Attributes;

import org.bandit.util.config.Realm;
import org.apache.log4j.Logger;

import java.io.Reader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Map;
import java.util.HashMap;

import com.novell.casa.NetCredential;
import com.novell.casa.MiCasa;

/**
 * RealmsInfo class.
 * <p>
 * Class for obtaining information about the configured realms.
 */
public class RealmsInfo
{
   private static final Logger m_log = Logger.getLogger(RealmsInfo.class);

   // Internal constants
   private final static String RealmUrl = "Url";
   private final static String RealmType = "Type";
   private final static String CntxtlessSearchString = "CntxtlessSearchString";

   /**
    * String returned by getType method for Active Directory realms.
    */
   public final static String ActiveDirectoryRealm = "ActiveDir";

   /**
    * String returned by getType method for eDirectory realms.
    */
   public final static String eDirectoryRealm = "eDir";

   /**
    * Internal class for holding information associated with a realm
    */
   private final class RealmInfo
   {
      // Map for holding ream key/values
      protected Map<String, String> m_keyValueMap = new HashMap<String, String>();
      protected String[] m_searchRoots = null;
      protected boolean m_proxyUserCredentialsInRealmsFile = true;
      protected String m_proxyUserName = null;
      protected String m_proxyUserPassword = null;
   }

   // Map for holding ream information
   private final Map<String, RealmInfo> m_realmsMap =  new HashMap<String, RealmInfo>();

   /**
    * Constructor.
    *
    * @param realmConfigFilePath    Path to iaRealms configuration file.
    * @throws Exception
    */
   RealmsInfo(String realmConfigFilePath) throws Exception
   {
      // Go through all of the configured realms
      Reader fileReader = null;
      try
      {
         fileReader = new InputStreamReader(new FileInputStream(realmConfigFilePath), "UTF8");
         RealmsType realmsType = Realms.unmarshal(fileReader);
         for (int i = 0; i < realmsType.getRealmsTypeItemCount(); i++)
         {
            RealmsTypeItem realmsTypeItem = realmsType.getRealmsTypeItem(i);
            org.bandit.util.config.gen.Realm realm = realmsTypeItem.getRealm();
            if (realm.getDesc().equalsIgnoreCase("Directory"))
            {
               // We are dealing with a directory realm
               RealmInfo realmInfo = new RealmInfo();
               RealmTypeItem[] realmTypeItems = realm.getRealmTypeItem();
               String principalName = null;
               String principalPassword = null;
               for (int ii = 0; ii < realmTypeItems.length; ii++)
               {
                  // Find the configure Proxy User Name for the realm and any configured
                  // search roots.
                  if (realmTypeItems[ii].getConnection() == null)
                  {
                     Env env[] = realmTypeItems[ii].getEnv();
                     if (env != null)
                     {
                        for (int iii = 0; iii < env.length; iii++)
                        {
                           if (env[iii].getProp().equalsIgnoreCase("java.naming.security.principal"))
                           {
                              // We found the proxy user name. Save it in case it is needed later.
                              principalName = env[iii].getValue();
                           }
                           else if (env[iii].getProp().equalsIgnoreCase("java.naming.security.credentials"))
                           {
                              // We found the proxy password. Save it in case it is needed later.
                              principalPassword = env[iii].getValue();
                           }
                           else if (env[iii].getProp().equalsIgnoreCase("com.novell.casa.authtoksvc.searchroot"))
                           {
                              // We are dealing with a search root, keep track of it. Make sure to preserve the order
                              // in which they are found.
                              if (realmInfo.m_searchRoots == null)
                              {
                                 realmInfo.m_searchRoots = new String[1];
                                 realmInfo.m_searchRoots[0] = env[iii].getValue();
                              }
                              else
                              {
                                 String[] newSearchRoots = new String[realmInfo.m_searchRoots.length + 1];
                                 System.arraycopy(realmInfo.m_searchRoots, 0, newSearchRoots, 0, realmInfo.m_searchRoots.length);
                                 newSearchRoots[realmInfo.m_searchRoots.length] = env[iii].getValue();
                                 realmInfo.m_searchRoots = newSearchRoots;
                              }
                           }
                           else if (env[iii].getProp().equalsIgnoreCase("com.novell.casa.authtoksvc.directory_type"))
                           {
                              // We are dealing with a directory type, decode it and record the result.
                              String realmDirectoryType = env[iii].getValue();
                              if (realmDirectoryType.equalsIgnoreCase("eDir"))
                              {
                                 realmInfo.m_keyValueMap.put(RealmType, eDirectoryRealm);
                              }
                              else if (realmDirectoryType.equalsIgnoreCase("ActiveDirectory"))
                              {
                                 realmInfo.m_keyValueMap.put(RealmType, ActiveDirectoryRealm);
                              }
                              else
                              {
                                 // Ignore parameter
                                 m_log.debug("Constructor()- Unknown directory type");
                              }
                           }
                           else if (env[iii].getProp().equalsIgnoreCase("com.novell.casa.authtoksvc.contextless_search_string"))
                           {
                              // We are dealing with the contextless search string, keep track of it.
                              realmInfo.m_keyValueMap.put(CntxtlessSearchString, env[iii].getValue());
                           }
                        }
                     }
                  }
                  else
                  {
                     LDAPConnector connector = (LDAPConnector) realmTypeItems[ii].getConnection();
                     realmInfo.m_keyValueMap.put(RealmUrl, connector.getAddress(0));
                  }
               }

               // Check if we need to read the proxy user credentials from miCASA
               if (principalName == null || principalPassword == null)
               {
                  realmInfo.m_proxyUserCredentialsInRealmsFile = false;

                  // We need to access miCASA to obtain the credentials
                  try
                  {
                     MiCasa store = new MiCasa();
                     NetCredential nc = store.getCredential(0, realm.getId(), null, 0, MiCasa.SSCS_CRED_TYPE_SERVER_F);
                     realmInfo.m_proxyUserName = principalName = nc.getUsername();
                     realmInfo.m_proxyUserPassword = principalPassword = nc.getPassword();
                  }
                  catch (Exception e)
                  {
                     m_log.warn("Constructor()- Exception caught obtaining proxy user credentials from miCASA for realm " + realm.getId() +", msg = " + e.getMessage());
                  }
               }

               // Check if we must try to determine the directory type
               if (realmInfo.m_keyValueMap.get(RealmType) == null)
               {
                  // The directory type has not been determined, check if the proxy username was configured.
                  if (principalName != null)
                  {
                     // The proxy user name was configured, use it to determine whether or not
                     // we are dealing with an Active Directory Server.
                     //
                     // Open a directory context and use it to read the "sAMAccountName"
                     // users attribute which theoretically should only be valid on an AD
                     // server.
                     Hashtable env = new Hashtable();
                     env.put(Context.INITIAL_CONTEXT_FACTORY, "org.bandit.ia.IAInitialCtxFactory");
                     env.put(Realm.REALM_CONFIG_LOCATION, realmConfigFilePath);
                     env.put(Realm.REALM_SELECTOR, realm.getId());
                     if (realmInfo.m_proxyUserCredentialsInRealmsFile == false)
                     {
                        env.put(Context.SECURITY_PRINCIPAL, realmInfo.m_proxyUserName);
                        env.put(Context.SECURITY_CREDENTIALS, realmInfo.m_proxyUserPassword);
                     }

                     int retries = 3;
                     while (retries != 0)
                     {
                        // Instantiate DirContext watching for an exception since it
                        // would be an indication that we should not retry the
                        // operation.
                        DirContext ctx;
                        try
                        {
                           ctx = new InitialDirContext(env);
                        }
                        catch (Exception e)
                        {
                           m_log.warn("Constructor()- Exception caught instantiating DirContext, msg = " + e.getMessage());

                           // Stop retrying
                           break;
                        }

                        // Now do the attribute lookup watching for a service unavailable
                        // exception as an indication that we should retry the operation.
                        try
                        {
                           String[] attributesNeeded  = new String[1];
                           attributesNeeded[0] = "sAMAccountName";
                           Attributes attributes = ctx.getAttributes(principalName, attributesNeeded);
                           NamingEnumeration ae = attributes.getAll();
                           if (ae != null && ae.hasMore())
                           {
                              realmInfo.m_keyValueMap.put(RealmType, ActiveDirectoryRealm);
                           }
                           else
                           {
                              realmInfo.m_keyValueMap.put(RealmType, eDirectoryRealm);
                           }
                        }
                        catch (ServiceUnavailableException e)
                        {
                           m_log.warn("Constructor()- ServiceUnavailable exception caught looking up attributes, msg = " + e.getMessage());

                           // Retry the operation
                           retries --;
                           continue;
                        }
                        catch (Exception e)
                        {
                           m_log.warn("Constructor()- Exception caught looking up attributes, msg = " + e.getMessage());
                        }

                        // No need to retry
                        break;
                     }
                  }
                  else
                  {
                     // The principal name was not configured, default to eDir.
                     realmInfo.m_keyValueMap.put(RealmType, eDirectoryRealm);
                  }
               }

               m_realmsMap.put(realm.getId(), realmInfo);
            }
         }
      }
      catch (Exception e)
      {
         m_log.error("Constructor()- Exception caught, msg = " + e.getMessage());
      }
      finally
      {
         if (fileReader != null)
         {
            try
            {
               fileReader.close();
            }
            catch (IOException e)
            {
               // Do nothing
            }
         }
      }
   }

   /**
    * Get Realm Type.
    *
    * @param realmId    Realm id.
    * @return           Realm type or null if no match found.
    */
   String getType(String realmId)
   {
      RealmInfo realmInfo = m_realmsMap.get(realmId);
      if (realmInfo != null)
         return realmInfo.m_keyValueMap.get(RealmType);
      else
         return null;
   }

   /**
    * Get Realm Url.
    *
    * @param realmId    Realm id.
    * @return           Realm url or null if no match found.
    */
   final String getUrl(String realmId)
   {
      RealmInfo realmInfo = m_realmsMap.get(realmId);
      if (realmInfo != null)
         return realmInfo.m_keyValueMap.get(RealmUrl);
      else
         return null;
   }

   /**
    * Get Search Roots.
    *
    * @param realmId    Realm id.
    * @return           String array with search roots or null if no match found.
    */
   final String[] getSearchRoots(String realmId)
   {
      RealmInfo realmInfo = m_realmsMap.get(realmId);
      if (realmInfo != null)
         return realmInfo.m_searchRoots;
      else
         return null;
   }

   /**
    * Get Contexless Search String.
    *
    * @param realmId    Realm id.
    * @return           ContextlessSearchString or null if no match found.
    */
   final String getCntxtlessSearchString(String realmId)
   {
      RealmInfo realmInfo = m_realmsMap.get(realmId);
      if (realmInfo != null)
         return realmInfo.m_keyValueMap.get(CntxtlessSearchString);
      else
         return null;
   }

   /**
    * Proxy Credentials In Realms File.
    *
    * @param realmId    Realm id.
    * @return           True if present.
    */
   final boolean proxyCredentialsInRealmFile(String realmId)
   {
      RealmInfo realmInfo = m_realmsMap.get(realmId);
      if (realmInfo != null)
         return realmInfo.m_proxyUserCredentialsInRealmsFile;
      else
         // Assume yes
         return true;
   }

   /**
    * Proxy Username Credential.
    *
    * @param realmId    Realm id.
    * @return           Username or null.
    */
   final String proxyUsernameCredential(String realmId)
   {
      RealmInfo realmInfo = m_realmsMap.get(realmId);
      if (realmInfo != null)
         return realmInfo.m_proxyUserName;
      else
         return null;
   }

   /**
    * Proxy Password Credential.
    *
    * @param realmId    Realm id.
    * @return           Password or null.
    */
   final String proxyPasswordCredential(String realmId)
   {
      RealmInfo realmInfo = m_realmsMap.get(realmId);
      if (realmInfo != null)
         return realmInfo.m_proxyUserPassword;
      else
         return null;
   }
}
