//
// Copyright (C) 2005 SIPfoundry Inc.
// Licensed by SIPfoundry under the LGPL license.
//
// Copyright (C) 2005 Pingtel Corp.
// Licensed to SIPfoundry under a Contributor Agreement.
//
// $$
//////////////////////////////////////////////////////////////////////////////

// SYSTEM INCLUDES
// APPLICATION INCLUDES
#include "os/OsReadLock.h"
#include "os/OsWriteLock.h"
#include "os/OsSysLog.h"
#include "os/OsConfigDb.h"
#include "utl/UtlSListIterator.h"
#include "utl/UtlHashMapIterator.h"
#include "net/XmlRpcDispatch.h"
#include "configrpc/ConfigRPC.h"

// EXTERNAL FUNCTIONS
// EXTERNAL VARIABLES
// CONSTANTS
const char* MethodName[ConfigRPC_Callback::NumMethods] =
{
   "configurationParameter.version",
   "configurationParameter.get",    
   "configurationParameter.set",    
   "configurationParameter.delete"
};

// STATICS

OsRWMutex* ConfigRPC::spDatabaseLock = new OsRWMutex(OsBSem::Q_PRIORITY);
UtlHashBag ConfigRPC::sDatabases;
bool       ConfigRPC::sRegistered = false;


/* //////////////////////////// PUBLIC //////////////////////////////////// */

/// Construct an instance to allow RPC access to a database.
ConfigRPC::ConfigRPC( const char*         dbName     ///< dbName known to XMLRPC methods
                     ,const char*         versionId  ///< version of this database
                     ,const UtlString&    dbPath     ///< path to persistent store for this db
                     ,ConfigRPC_Callback* callback   ///< connection to controlling application
                     )
   :UtlString(dbName)
   ,mVersion(versionId)
   ,mPath(dbPath)
   ,mCallback(callback)
{
   assert(dbName && *dbName != '\000');
   assert(versionId && *versionId != '\000');
   assert(!dbPath.isNull());
   assert(callback);
   
   OsWriteLock lock(*spDatabaseLock);

   if ( ! sDatabases.find(this) )
   {
      OsSysLog::add( FAC_KERNEL, PRI_INFO, "ConfigRPC:: register access to db name '%s'", dbName);
      sDatabases.insert(this);
   }
   else
   {
      OsSysLog::add( FAC_KERNEL, PRI_CRIT, "ConfigRPC:: duplicate db name '%s'", dbName);
   }
}

OsStatus ConfigRPC::load(OsConfigDb& dataset)
{
   OsStatus status = dataset.loadFromFile(mPath);
   if ( OS_SUCCESS != status )
   {
      OsSysLog::add(FAC_KERNEL, PRI_ERR,
                    "ConfigRPC failed to load '%s' from '%s'",
                    data(), mPath.data()
                    );
   }
   return status;
}


OsStatus ConfigRPC::store(OsConfigDb& dataset)
{
   OsStatus status = dataset.storeToFile(mPath);
   if ( OS_SUCCESS != status )
   {
      OsSysLog::add(FAC_KERNEL, PRI_ERR,
                    "ConfigRPC failed to store '%s' to '%s'",
                    data(), mPath.data()
                    );
   }
   return status;
}

/// Destroy the instance to disconnect access to the database.
ConfigRPC::~ConfigRPC()
{
   OsWriteLock lock(*spDatabaseLock);
   sDatabases.remove(this);
}

/*****************************************************************
 * Default ConfigRPC_Callback
 *****************************************************************/

ConfigRPC_Callback::ConfigRPC_Callback()
{
}
   
// Access check function 
XmlRpcMethod::ExecutionStatus ConfigRPC_Callback::accessAllowed(
   const HttpRequestContext& requestContext,
   Method                    method
                                                                ) const
{
   OsSysLog::add(FAC_KERNEL, PRI_INFO,
                 "ConfigRPC default accessAllowed for %s",
                 MethodName[method]
                 );
   return XmlRpcMethod::OK;
}

/// Invoked after the database has been modified
void ConfigRPC_Callback::modified()
{
   // in the abstract base class this is a no-op 
   OsSysLog::add(FAC_KERNEL, PRI_INFO, "ConfigRPC default modified");
}
   

ConfigRPC_Callback::~ConfigRPC_Callback()
{
}

/// Instantiate this to allow configuration from hosts in the same SIP domain
ConfigRPC_InDomainCallback::ConfigRPC_InDomainCallback(const UtlString& domain ///< to allow
                                                       )
   :mAllowedDomain(domain)
{
}


/// Access check function 
XmlRpcMethod::ExecutionStatus ConfigRPC_InDomainCallback::accessAllowed(
   const HttpRequestContext&  requestContext,
   ConfigRPC_Callback::Method method
                                                                        ) const
{
   XmlRpcMethod::ExecutionStatus isAllowed = (  requestContext.isTrustedPeer(mAllowedDomain)
                                              ? XmlRpcMethod::OK
                                              : XmlRpcMethod::FAILED
                                              );
   /*
    * - XmlRpcMethod::OK if allowed
    * - XmlRpcMethod::FAILED if not allowed,
    * - XmlRpcMethod::REQUIRE_AUTHENTICATION if authentication is missing or invalid.
    */
   if (XmlRpcMethod::FAILED == isAllowed)
   {
      OsSysLog::add(FAC_KERNEL, PRI_WARNING,
                    "ConfigRPC_InDomainCallback disallowed configuration from untrusted peer"
                    );
   }
   return isAllowed;
}



/// Implements the XML-RPC method configurationParameter.version
/**
 * Inputs:
 *  string    dbName           Name of the database.
 *  Outputs:
 *  string    version_id       The version identifier.
 *
 *  This method should be called by a configuration application to
 *  confirm that it is using a compatible definition of the database
 *  definition.  If the returned version_id does not match what the 
 *  configuration application expects, it should not modify the configuration..
 */
class ConfigRPC_version : public XmlRpcMethod
{
public:
   static XmlRpcMethod* get()
      {
         return new ConfigRPC_version();
      }

protected:
   virtual bool execute(const HttpRequestContext& requestContext, ///< request context
                        UtlSList& params,                         ///< request param list
                        void* userData,                           ///< user data
                        XmlRpcResponse& response,                 ///< request response
                        ExecutionStatus& status
                        )
      {
         UtlString* dbName = dynamic_cast<UtlString*>(params.at(0));

         if (dbName && !dbName->isNull())
         {
            OsReadLock lock(*ConfigRPC::spDatabaseLock);
            ConfigRPC* db = ConfigRPC::find(*dbName);

            if (db)
            {
               status = db->mCallback->accessAllowed(requestContext, ConfigRPC_Callback::Version);
               if ( XmlRpcMethod::OK == status )
               {
                  response.setResponse(&db->mVersion);
               }
               else
               {
                  UtlString faultMsg("Access Denied");
                  response.setFault(XmlRpcMethod::FAILED, faultMsg.data());
               }
            }
            else
            {
               UtlString faultMsg;               
               faultMsg.append("db lookup failed for '");
               faultMsg.append(*dbName);
               faultMsg.append("'");
               response.setFault( XmlRpcResponse::UnregisteredMethod, faultMsg.data());
               status = XmlRpcMethod::FAILED;
            }
         }
         else
         {
            response.setFault( XmlRpcResponse::EmptyParameterValue
                              ,"'dbname' parameter is missing or invalid type"
                              );
            status = XmlRpcMethod::FAILED;
         }
         
         return true;
      }
};


/// Implements the XML-RPC method configurationParameter.get
/*
 * Parameters  Type      Name             Description
 *  Inputs:
 *             string    db_name          configuration data set name
 *             array
 *               string  parameter        name of parameter to return
 *  Outputs:
 *             struct
 *               string  parameter        parameter name (key)
 *               string  value            parameter value
 *
 * Returns the name and value for each parameter in the input array
 * of parameter names.  If any parameter in the set is undefined, a
 * PARAMETER_UNDEFINED fault is returned.
 *
 * To get all the parameters in the database, call this method with just the db_name.
 * When called with just the db_name, if the dataset is empty (there are no parameters
 * defined), a DATASET_EMPTY fault is returned.
 */
class ConfigRPC_get : public XmlRpcMethod
{
public:

   static XmlRpcMethod* get()
      {
         return new ConfigRPC_get();
      }

protected:
   virtual bool execute(const HttpRequestContext& requestContext, ///< request context
                        UtlSList& params,                         ///< request param list
                        void* userData,                           ///< user data
                        XmlRpcResponse& response,                 ///< request response
                        ExecutionStatus& status
                        )
      {
         UtlString* dbName = dynamic_cast<UtlString*>(params.at(0));

         if (dbName && !dbName->isNull())
         {
            OsReadLock lock(*ConfigRPC::spDatabaseLock);

            // find the dataset registered with this name
            ConfigRPC* db = ConfigRPC::find(*dbName);
            if (db)
            {
               // check with the application to see if this request is authorized on this dataset
               status = db->mCallback->accessAllowed(requestContext, ConfigRPC_Callback::Get);
               if ( XmlRpcMethod::OK == status )
               {
                  // read in the dataset
                  OsConfigDb dataset;
                  OsStatus datasetStatus = db->load(dataset);
                  if ( OS_SUCCESS == datasetStatus )
                  {
                     // get the list of names that the request is asking for 
                     UtlContainable* secondParam = params.at(1);
                     if ( secondParam )
                     {
                        UtlSList* nameList = dynamic_cast<UtlSList*>(secondParam);
                        if (nameList)
                        {
                           /*
                            * Iterate over the requested names
                            * - All must be present or the request is an error
                            * - For each name found, add the name and value to the
                            *   selectedParams hash to be returned in a success response.
                            */
                           UtlHashMap selectedParams;
                           UtlSListIterator requestedNames(*nameList);
                           UtlString* requestedName = NULL;
                           bool allNamesFound = true;
                           
                           while (   allNamesFound
                                  && (requestedName = dynamic_cast<UtlString*>(requestedNames()))
                                  )
                           {
                              UtlString* paramValue = new UtlString();
                              if ( OS_SUCCESS == dataset.get(*requestedName, *paramValue) )
                              {
                                 UtlString* paramName  = new UtlString(*requestedName);
                                 // put it into the results
                                 selectedParams.insertKeyAndValue(paramName, paramValue);
                              }
                              else
                              {
                                 allNamesFound = false;
                                 delete paramValue;
                              }
                           }

                           if (allNamesFound)
                           {
                              // all were found - return the name/value pairs
                              response.setResponse(&selectedParams);
                           }
                           else
                           {
                              // at least one name was not found - return an error.
                              UtlString faultMsg;
                              faultMsg.append("parameter name '");
                              faultMsg.append(*requestedName);
                              faultMsg.append("' not found");
                              response.setFault(ConfigRPC::nameNotFound, faultMsg.data());
                              status = XmlRpcMethod::FAILED;
                           }
                           
                           selectedParams.destroyAll();
                        }
                        else
                        {
                           // The second parameter was not a list
                           response.setFault( ConfigRPC::invalidType
                                             ,"namelist parameter is not an array"
                                             );
                           status = XmlRpcMethod::FAILED;
                        }
                     }
                     else // no parameter names specified
                     {
                        // return all names
                        UtlHashMap allParams;
                        UtlString  lastKey;
                        OsStatus   iterateStatus;
                        UtlString* paramName;
                        UtlString* paramValue;
                        bool       notEmpty = false;
  
                        for ( ( paramName  = new UtlString()
                               ,paramValue = new UtlString()
                               ,iterateStatus = dataset.getNext(lastKey, *paramName, *paramValue)
                               );
                              OS_SUCCESS == iterateStatus;
                              ( lastKey       = *paramName
                               ,paramName     = new UtlString()
                               ,paramValue    = new UtlString()
                               ,iterateStatus = dataset.getNext(lastKey, *paramName, *paramValue)
                               )
                             )
                        {
                           notEmpty = true; // got at least one parameter
                           // put it into the result array
                           allParams.insertKeyAndValue(paramName, paramValue);
                        }
                        // on the final iteration these were not used
                        delete paramName;
                        delete paramValue;
                        
                        if (notEmpty)
                        {
                           response.setResponse(&allParams);
                           allParams.destroyAll();
                        }
                        else
                        {
                           // there is no way to send a well-formed but empty response,
                           // so a 'get all' on an empty dataset returns a fault.
                           UtlString faultMsg;               
                           faultMsg.append("dataset '");
                           faultMsg.append(*dbName);
                           faultMsg.append("' has no parameters");
                           response.setFault(ConfigRPC::emptyDataset, faultMsg);
                           status = XmlRpcMethod::FAILED;
                        }
                     }
                  }
                  else
                  {
                     UtlString faultMsg("dataset load failed");
                     response.setFault(ConfigRPC::loadFailed, faultMsg);
                     status = XmlRpcMethod::FAILED;
                  }
               }
               else
               {
                  UtlString faultMsg("Access Denied");
                  response.setFault(XmlRpcMethod::FAILED, faultMsg.data());
               }
            }
            else
            {
               UtlString faultMsg;               
               faultMsg.append("db lookup failed for '");
               faultMsg.append(*dbName);
               faultMsg.append("'");
               response.setFault( XmlRpcResponse::UnregisteredMethod, faultMsg.data());
               status = XmlRpcMethod::FAILED;
            }
         }
         else
         {
            response.setFault( XmlRpcResponse::EmptyParameterValue
                              ,"'dbname' parameter is missing or invalid type"
                              );
            status = XmlRpcMethod::FAILED;
         }
         
         return true;
      }
};

/// Implements the XML-RPC method configurationParameter.set method
/**
 *  Parameters  Type      Name             Description
 *  Inputs:
 *              string    db_name          configuration data set name
 *              struct
 *                string  parameter      parameter name (key)
 *                string  value          parameter value
 *    ...
 *  Outputs:
 *              integer                    number of values set
 *
 *  Sets each 'parameter' / 'value' pair in 'db_name'.  Either all
 *  sets are made or none are made.
 */
class ConfigRPC_set : public XmlRpcMethod
{
public:
   static XmlRpcMethod* get()
      {
         return new ConfigRPC_set();
      }

protected:
   virtual bool execute(const HttpRequestContext& requestContext, ///< request context
                        UtlSList& params,                         ///< request param list
                        void* userData,                           ///< user data
                        XmlRpcResponse& response,                 ///< request response
                        ExecutionStatus& status
                        )
      {
         UtlString* dbName = dynamic_cast<UtlString*>(params.at(0));

         if (dbName && !dbName->isNull())
         {
            OsReadLock lock(*ConfigRPC::spDatabaseLock);

            ConfigRPC* db = ConfigRPC::find(*dbName);
            if (db)
            {
               status = db->mCallback->accessAllowed(requestContext, ConfigRPC_Callback::Set);
               if ( XmlRpcMethod::OK == status )
               {
                  // read in the dataset
                  OsConfigDb dataset;
                  OsStatus datasetStatus = db->load(dataset);
                  if ( OS_SUCCESS == datasetStatus )
                  {
                     // get the list of names that the request is asking for 
                     UtlContainable* secondParam = params.at(1);
                     if ( secondParam )
                     {
                        UtlHashMap* paramList = dynamic_cast<UtlHashMap*>(secondParam);
                        if (paramList)
                        {
                           /*
                            * Iterate over the requested name/value pairs
                            */
                           UtlHashMapIterator params(*paramList);
                           UtlContainable* nextParam = NULL;
                           size_t paramsSet = 0;
                           
                           while (    XmlRpcMethod::OK == status
                                  && (nextParam = params())
                                  )
                           {
                              UtlString* name = dynamic_cast<UtlString*>(params.key());
                              if ( name )
                              {
                                 UtlString* value = dynamic_cast<UtlString*>(params.value());
                                 if (value)
                                 {
                                    dataset.set(*name, *value);
                                    paramsSet++;
                                 }
                                 else
                                 {
                                    UtlString faultMsg;
                                    faultMsg.append("parameter name '");
                                    faultMsg.append(*name);
                                    faultMsg.append("' value is not a string");
                                    response.setFault(ConfigRPC::invalidType, faultMsg.data());
                                    status = XmlRpcMethod::FAILED;
                                 }
                              }
                              else
                              {
                                 UtlString faultMsg;
                                 faultMsg.append("parameter number ");
                                 char paramIndex[10];
                                 sprintf(paramIndex,"%d", paramsSet + 1);
                                 faultMsg.append(paramIndex);
                                 faultMsg.append(" name is not a string");
                                 response.setFault(ConfigRPC::invalidType, faultMsg.data());
                                 status = XmlRpcMethod::FAILED;
                              }
                           }

                           if ( XmlRpcMethod::OK == status )
                           {
                              if (OS_SUCCESS == db->store(dataset))
                              {
                                 UtlInt numberSet(paramList->entries());
                                 response.setResponse(&numberSet);
                              }
                              else
                              {
                                 response.setFault( ConfigRPC::storeFailed
                                                   ,"error storing dataset"
                                                   );
                                 status = XmlRpcMethod::FAILED;
                              }
                           }
                        }
                        else
                        {
                           // The second parameter was not a list
                           response.setFault( ConfigRPC::invalidType
                                             ,"second parameter is not a struct"
                                             );
                           status = XmlRpcMethod::FAILED;
                        }
                     }
                     else // no parameter names specified
                     {
                        // No second parameter
                        response.setFault( ConfigRPC::invalidType
                                          ,"no second parameter of name/value pairs"
                                          );
                        status = XmlRpcMethod::FAILED;
                     }
                  }
                  else
                  {
                     UtlString faultMsg("dataset load failed");
                     response.setFault(ConfigRPC::loadFailed, faultMsg);
                     status = XmlRpcMethod::FAILED;
                  }
               }
               else
               {
                  UtlString faultMsg("Access Denied");
                  response.setFault(XmlRpcMethod::FAILED, faultMsg.data());
               }
            }
            else
            {
               UtlString faultMsg;               
               faultMsg.append("db lookup failed for '");
               faultMsg.append(*dbName);
               faultMsg.append("'");
               response.setFault( XmlRpcResponse::UnregisteredMethod, faultMsg.data());
               status = XmlRpcMethod::FAILED;
            }
         }
         else
         {
            response.setFault( XmlRpcResponse::EmptyParameterValue
                              ,"'dbname' parameter is missing or invalid type"
                              );
            status = XmlRpcMethod::FAILED;
         }
         
         return true;
      }
};

/*

  Method: configurationParameter.delete

  Parameters  Type      Name             Description
  Inputs:
  string    db_name          configuration data set name
  string    parameter        parameter name (key)
  Outputs:
  (none)

  Removes 'parameter' from 'db_name'.  This causes the value of
  'parameter' to be undefined.  It is not an error to invoke the
  delete method on an undefined parameter.

*/
class ConfigRPC_delete : public XmlRpcMethod
{
public:
   static XmlRpcMethod* get()
      {
         return new ConfigRPC_delete();
      }

protected:
   virtual bool execute(const HttpRequestContext& requestContext, ///< request context
                        UtlSList& params,                         ///< request param list
                        void* userData,                           ///< user data
                        XmlRpcResponse& response,                 ///< request response
                        ExecutionStatus& status
                        )
      {
         UtlString* dbName = dynamic_cast<UtlString*>(params.at(0));

         if (dbName && !dbName->isNull())
         {
            OsReadLock lock(*ConfigRPC::spDatabaseLock);

            ConfigRPC* db = ConfigRPC::find(*dbName);
            if (db)
            {
               status = db->mCallback->accessAllowed(requestContext, ConfigRPC_Callback::Set);
               if ( XmlRpcMethod::OK == status )
               {
                  // read in the dataset
                  OsConfigDb dataset;
                  OsStatus datasetStatus = db->load(dataset);
                  if ( OS_SUCCESS == datasetStatus )
                  {
                     // get the list of names that the request is trying to delete
                     UtlContainable* secondParam = params.at(1);
                     if ( secondParam )
                     {
                        UtlSList* nameList = dynamic_cast<UtlSList*>(secondParam);
                        if (nameList)
                        {
                           /*
                            * Iterate over the names
                            * - For each name found, delete it from the dataset and count it
                            */
                           UtlSListIterator deleteNames(*nameList);
                           UtlString* deleteName = NULL;
                           size_t deleted = 0;
                           
                           while ((deleteName = dynamic_cast<UtlString*>(deleteNames())))
                           {
                              if (OS_SUCCESS == dataset.remove(*deleteName))
                              {
                                 deleted++;
                              }
                           }

                           if (OS_SUCCESS == db->store(dataset))
                           {
                              status = XmlRpcMethod::OK;
                              UtlInt deletedCount(deleted);
                              response.setResponse(&deletedCount);
                           }
                           else
                           {
                              response.setFault( ConfigRPC::storeFailed
                                                ,"error storing dataset"
                                                );
                              status = XmlRpcMethod::FAILED;
                           }
                        }
                        else
                        {
                           // The second parameter was not a list
                           response.setFault( ConfigRPC::invalidType
                                             ,"namelist parameter is not an array"
                                             );
                           status = XmlRpcMethod::FAILED;
                        }
                     }
                     else // No second parameter
                     {
                        response.setFault( ConfigRPC::invalidType
                                          ,"no second parameter list of names to delete"
                                          );
                        status = XmlRpcMethod::FAILED;
                     }
                  }
                  else
                  {
                     UtlString faultMsg("dataset load failed");
                     response.setFault(ConfigRPC::loadFailed, faultMsg);
                     status = XmlRpcMethod::FAILED;
                  }
               }
               else
               {
                  UtlString faultMsg("Access Denied");
                  response.setFault(XmlRpcMethod::FAILED, faultMsg.data());
               }
            }
            else
            {
               UtlString faultMsg;               
               faultMsg.append("db lookup failed for '");
               faultMsg.append(*dbName);
               faultMsg.append("'");
               response.setFault( XmlRpcResponse::UnregisteredMethod, faultMsg.data());
               status = XmlRpcMethod::FAILED;
            }
         }
         else
         {
            response.setFault( XmlRpcResponse::EmptyParameterValue
                              ,"'dbname' parameter is missing or invalid type"
                              );
            status = XmlRpcMethod::FAILED;
         }
         
         return true;
      }
};


// Must be called once to connect the configurationParameter methods
void ConfigRPC::registerMethods(XmlRpcDispatch&     rpc /* xmlrpc dispatch service to use */)
{
   OsWriteLock lock(*spDatabaseLock);

   if (!sRegistered)
   {
      rpc.addMethod(MethodName[ConfigRPC_Callback::Version], ConfigRPC_version::get, NULL);
      rpc.addMethod(MethodName[ConfigRPC_Callback::Get],     ConfigRPC_get::get,     NULL);
      rpc.addMethod(MethodName[ConfigRPC_Callback::Set],     ConfigRPC_set::get,     NULL);
      rpc.addMethod(MethodName[ConfigRPC_Callback::Delete],  ConfigRPC_delete::get,  NULL);

      sRegistered = true;
   }
}



syntax highlighted by Code2HTML, v. 0.9.1