perfectxml.com
 Basic Search  Advanced Search   
Topics Resources Free Library Software XML News About Us
home » info bank » articles » Working with XML-based Configuration Files Mon, Oct 29, 2007
Working with XML-based Configuration Files

Abstract: XML-formatted files are fast becoming the preferred method of saving the configuration settings. The reasons for this include ease of parsing and processing, ability to transform (for instance to generate HTML report from the config settings), extensibility, and so on.

The .NET Framework provides System.Configuration namespace, which contains various classes to work with XML-based configuration files.

However, if you are working on a non-.NET application and need to use XML-based configuration files, this article contains a C++ class that will make your life easier. The class CConfigurationSettings allows loading local XML file, XML string, or remote XML file over HTTP. The methods allow getting and setting "standard" and "custom" configuration values; and persisting the configuration changes. The class also supports namespaces.
Author: perfectxml.com Team
Last Updated: March 01, 2003
Download: ConfigFiles.zip (106 KB ZIP file)

If you have worked with the .NET Framework, especially with ASP.NET, the concept of XML-based configuration files should sound familiar. For each ASP.NET Web site or Web service, the Framework provides a file web.config, an XML file, that contains information such as session settings, tracing and debugging settings, authorization and authentication information, and other custom settings written as the <add…/> tags within the <appSettings> tag.

However, for C++ or VB developers (non-.NET), there is no such thing to work with XML-based configuration files. The goal of this article is to write a simple C++ class that uses MSXML 4.0 and provides methods to work with XML-based configuration files.

Let's begin by looking at the class definition (ConfigurationSettings.h):

The class header file includes atlbase.h as we'll be using ATL templates and macros, and the #import statement is used to import the MSXML type library.
#if !defined(CONFIGURATION_SETTINGS_INCLUDED)
#define CONFIGURATION_SETTINGS_INCLUDED

#include "tchar.h"
#include "atlbase.h"

#import <msxml4.dll> named_guids
using namespace MSXML2;
The class definition begins with the constructor and destructor declarations. The constructor accepts two parameter – both are optional. The first parameter specifies the source for the XML configuration file and the second parameter specifies the type of source (local file, string, or HTTP URL). If the first parameter is omitted, the class would try to load the <appName.exe>.config file from the same folder where the application exe, which uses this class, is present.
class CConfigurationSettings
{
public:
   //Methods

   //------------------------------------------------------------------
   //Constructor that accepts 
   //      Config XML file name with the path (iSourceType=1) (default)
   //      Config XML String (iSourceType=2)
   //      Config XML to be loaded from remote HTTP Server (iSourceType=3)
   //   
   //If lpszConfigSrc parameter is ignored, tries to load <appname.exe>.config
   //For ex, if this class is used in application named Test.exe, and if 
   //no parameters are specified, it tries to load Test.exe.config file.
   //------------------------------------------------------------------
   CConfigurationSettings(LPCTSTR lpszConfigSrc=NULL, int iSourceType=1);

   //Destructor
   virtual ~CConfigurationSettings();
The class contains six public methods: two methods to set and get configuration values based on the pre-defined path in the XML configuration file, two methods to get configuration values from anywhere, based on the provided XPath expression, and one method to set the namespaces to be used, finally a method to save the configuration XML file.
   virtual LPTSTR GetConfigValue(LPCTSTR lpszXPathExpr);
   virtual LPTSTR GetAppSetting(LPCTSTR lpszConfigKeyName);

   virtual bool SetConfigValue(LPCTSTR lpszXPathExpr, LPCTSTR lpszNewValue);
   virtual bool SetAppSetting(LPCTSTR lpszConfigKeyName, LPCTSTR lpszNewValue);

   virtual bool SetSelectionNamespaces(LPCTSTR lpszNamespaces);

   virtual bool SaveConfigFile(LPCTSTR lpszConfigFile=NULL);

public:
   //Properties
   TCHAR m_lpszLastError[MAX_PATH*4];
   HRESULT m_hLastError;

   TCHAR m_lpszAppSettingsXPath[MAX_PATH];

protected:
   //The XML DOMDocument object 
   CComPtr<IXMLDOMDocument2> m_spXMLDoc;

   //If the Config Source is successfully loaded
   bool m_bConfigSourceLoaded;

};

#endif //CONFIGURATION_SETTINGS_INCLUDED

Let's now look at the implementation CPP file (ConfigurationSettings.cpp).
#include "ConfigurationSettings.h"

#define CHECK_HR_THROW(hr) { if (FAILED(hr)) { throw -1; } }
Let's begin with the most important part: the constructor. The job of constructor is to create the MSXML DOMDocument object, and load the configuration XML source local file, string, or remote HTTP URL file, based on the second parameter value. If no parameters are passed, then get the application exe path and file name, attach .config to it, and try to load the XML file with this name.
CConfigurationSettings::CConfigurationSettings(LPCTSTR lpszConfigSrc, int iSourceType):
  m_bConfigSourceLoaded(false), m_hLastError(S_OK)
{
  m_lpszLastError[0] = 0;

  //Default XPath to be used for [Get/Set/Remove]AppSetting methods
  _tcscpy(m_lpszAppSettingsXPath, _T("/configuration/appSettings/add[@key='%s']/@value"));

  USES_CONVERSION;

  try
  {
    //Step 1: Create MSXML DOMDocument Object
    m_hLastError = m_spXMLDoc.CoCreateInstance(CLSID_DOMDocument40);
    CHECK_HR_THROW(m_hLastError);

    //Load file synchronously, avoid validation, avoid external references resolution
    m_spXMLDoc->async = VARIANT_FALSE;
    m_spXMLDoc->validateOnParse = VARIANT_FALSE;
    m_spXMLDoc->resolveExternals = VARIANT_FALSE;

    //We'll be loading document synchronously, and will not use DTD
    //Hence we can safely set NewParser property to true, to get MSXML 4.0 SP1 speed benefits
    m_hLastError = m_spXMLDoc->setProperty(_bstr_t(_T("NewParser")), VARIANT_TRUE);
    CHECK_HR_THROW(m_hLastError);

    //Set the SelectionLanguage to XPath
    m_hLastError = m_spXMLDoc->setProperty(_bstr_t(_T("SelectionLanguage")), 
      CComVariant(_T("XPath")) );
    CHECK_HR_THROW(m_hLastError);


    //Step 2: Based on iSourceType, load XML Document
    TCHAR lpszLocalXMLFile[MAX_PATH*4] = {0};
    switch(iSourceType)
    {
      case 1://Local Configuration XML file
        
        if(lpszConfigSrc == NULL || (lpszConfigSrc != NULL && _tcslen(lpszConfigSrc) <= 0))
        {//Config File Name ommitted
          GetModuleFileName(NULL, lpszLocalXMLFile, MAX_PATH*2);
          _tcscat(lpszLocalXMLFile, _T(".config"));
        }
        else
        {//Config File Name specified
          _tcscpy(lpszLocalXMLFile, lpszConfigSrc);
        }

        //Try to load the local XML file
        if(m_spXMLDoc->load(CComVariant(lpszLocalXMLFile)) == VARIANT_TRUE)
        {
          m_bConfigSourceLoaded = true;
        }
        else
        {
          m_hLastError = E_FAIL;
          sprintf(m_lpszLastError, _T("Failed to load XML file '%s'. %s"), 
            lpszLocalXMLFile,
            W2A(m_spXMLDoc->parseError->reason));
        }

        break;

      case 2://Configuration XML String
        //Try to load the input XML string
        if(lpszConfigSrc == NULL || (lpszConfigSrc != NULL && _tcslen(lpszConfigSrc) <= 0))
        {
          m_hLastError = E_FAIL;
          sprintf(m_lpszLastError, _T("Missing Configuration XML string value."));
        }
        else
        {
          if(m_spXMLDoc->loadXML(_bstr_t(lpszConfigSrc)) == VARIANT_TRUE)
          {
            m_bConfigSourceLoaded = true;
          }
          else
          {
            m_hLastError = E_FAIL;
            sprintf(m_lpszLastError, _T("Failed to load Config XML string. '%s'"), 
              W2A(m_spXMLDoc->parseError->reason) );
          }
        }
        
        break;

      case 3://Remote HTTP URL
        if(lpszConfigSrc == NULL || (lpszConfigSrc != NULL && _tcslen(lpszConfigSrc) <= 0))
        {
          m_hLastError = E_FAIL;
          sprintf(m_lpszLastError, _T("Missing Configuration XML File URL."));
        }
        else
        {
          //Since its a remote HTTP file, set the ServerHTTPRequest property to true
          m_hLastError = m_spXMLDoc->setProperty(_T("ServerHTTPRequest"), VARIANT_TRUE);
          CHECK_HR_THROW(m_hLastError);

          //Try to load the remote XML file
          if(m_spXMLDoc->load(CComVariant(lpszConfigSrc)) == VARIANT_TRUE)
          {
            m_bConfigSourceLoaded = true;
          }
          else
          {
            m_hLastError = E_FAIL;
            sprintf(m_lpszLastError, _T("Failed to load remote XML file '%s'. %s"), 
              lpszConfigSrc,
              W2A(m_spXMLDoc->parseError->reason));
          }
        }

        break;

      default://Incorrect parameter
        sprintf(m_lpszLastError, _T("Incorrect parameter %d"), iSourceType);
        ATLASSERT(0);
        break;
    }

  }
  catch(...)
  {
  }
}
Once the object is created, and constructor is called, the class member variables m_hLastError and m_lpszLastError can be used to find out if there were any errors in creating the DOMDocument or loading the XML file.

The SetSelectionNamespaces method simply calls the DOMDocument setProperty method:
bool CConfigurationSettings::SetSelectionNamespaces(LPCTSTR lpszNamespaces)
{
  ATLASSERT(m_spXMLDoc.p != NULL);
  ATLASSERT(m_bConfigSourceLoaded == true);
  ATLASSERT(lpszNamespaces != NULL);

  bool bRetVal = false;
  if(lpszNamespaces && m_spXMLDoc && m_bConfigSourceLoaded)
  {
    m_hLastError = m_spXMLDoc->setProperty(_bstr_t(_T("SelectionNamespaces")), 
      CComVariant(lpszNamespaces));
    CHECK_HR_THROW(m_hLastError);

    bRetVal = true;
  }

  return bRetVal;
}
Let's now look at the GetConfigValue and SetConfigValue members. These methods work on the input XPath expression. After checking that that XML document is loaded fine, these methods use the selectSingleNode method to query for the node, and if found, it uses the nodeTypedValue and text properties to get and set the node values, respectively.
LPTSTR CConfigurationSettings::GetConfigValue(LPCTSTR lpszXPathExpr)
{
  CComPtr<IXMLDOMNode> spMatchingNode;
  USES_CONVERSION;

  ATLASSERT(m_spXMLDoc.p != NULL);
  ATLASSERT(m_bConfigSourceLoaded == true);
  ATLASSERT(lpszXPathExpr != NULL);

  //Evaluate XPath expression, and get the matching node
  if(m_spXMLDoc && m_bConfigSourceLoaded  && lpszXPathExpr)
    spMatchingNode =  m_spXMLDoc->selectSingleNode(_bstr_t(lpszXPathExpr));

  //if matching node found
  if(spMatchingNode)
  {
    TCHAR* lpszReturnValue = new TCHAR[MAX_PATH*2];
    lpszReturnValue[0] = 0;
    
    CComVariant cvNodeTypedValue = spMatchingNode->nodeTypedValue;;

    if(cvNodeTypedValue.vt == VT_BSTR)
      _tcscpy(lpszReturnValue, W2A(cvNodeTypedValue.bstrVal));

    return lpszReturnValue;
  }
  else
    return NULL;
}


bool CConfigurationSettings::SetConfigValue(LPCTSTR lpszXPathExpr, LPCTSTR lpszNewValue)
{
  bool bRetVal = false;
  CComPtr<IXMLDOMNode> spMatchingNode;
  USES_CONVERSION;

  ATLASSERT(m_spXMLDoc.p != NULL);
  ATLASSERT(m_bConfigSourceLoaded == true);
  ATLASSERT(lpszXPathExpr != NULL);

  if(m_spXMLDoc && m_bConfigSourceLoaded  && lpszXPathExpr)
    spMatchingNode =  m_spXMLDoc->selectSingleNode(_bstr_t(lpszXPathExpr));

  if(spMatchingNode)
  {
    spMatchingNode->text = _bstr_t(lpszNewValue);
    return true;
  }

  return bRetVal;
}
Setting the configuration value by calling the above method (SetConfigValue), only updates the XML document in the memory. To persist the changes into the configuration file, the client of this class needs to call the SaveConfigFile method, which accepts filename as the parameter. If this is omitted, the method uses the DOMDocument url property to get the location from where the file was loaded. The parameter can omitted only for local files. If the document was loaded by passing a string or over HTTP, a valid file name needs to be passed to this function, for it to succeed.
bool CConfigurationSettings::SaveConfigFile(LPCTSTR lpszConfigFile)
{
  ATLASSERT(m_spXMLDoc.p != NULL);
  ATLASSERT(m_bConfigSourceLoaded == true);

  bool bRetVal = false;
  //If lpszConfigFile param ommitted, save to the same location from 
  //where the file was loaded - works for local files only (iSourceType=1)
  if(lpszConfigFile == NULL && m_spXMLDoc && m_bConfigSourceLoaded)
  {
    m_hLastError = m_spXMLDoc->save(m_spXMLDoc->url);
  }
  else if(lpszConfigFile && m_spXMLDoc && m_bConfigSourceLoaded)
  {
    m_hLastError = m_spXMLDoc->save(CComVariant(lpszConfigFile));
  }
  CHECK_HR_THROW(m_hLastError);

  return bRetVal;
}
If you closely look at the constructor, as part of initialization, it initializes the member m_lpszAppSettingsXPath as below:
  _tcscpy(m_lpszAppSettingsXPath, _T("/configuration/appSettings/add[@key='%s']/@value"));
This member is used to get/set the configuration values from the standard pre-defined location by calling the following members: GetAppSetting and SetAppSetting. These method relieve the client of this class from having to worry about XPath expression, and require it to just pass the key name to set and get the configuration values.
LPTSTR CConfigurationSettings::GetAppSetting(LPCTSTR lpszConfigKeyName)
{
  if(lpszConfigKeyName)
  {
    TCHAR lpszXPathExpr[MAX_PATH*2] = {0};
    sprintf(lpszXPathExpr, m_lpszAppSettingsXPath, lpszConfigKeyName);

    return GetConfigValue(lpszXPathExpr);
  }

  return NULL;
}


bool CConfigurationSettings::SetAppSetting(LPCTSTR lpszConfigKeyName, LPCTSTR lpszNewValue)
{
  bool bRetVal = false;
  if(lpszConfigKeyName)
  {
    TCHAR lpszXPathExpr[MAX_PATH*2] = {0};
    sprintf(lpszXPathExpr, m_lpszAppSettingsXPath, lpszConfigKeyName);

    return SetConfigValue(lpszXPathExpr, lpszNewValue);
  }

  return bRetVal;
}
The above functions make use of the m_lpszAppSettingsXPath member to construct an XPath expression, and then simply call GetConfigValue or SetConfigValue.

This concludes our discussion on the class, let's finally look at a console application which uses this class to work with the XML-based configuration file:
#include "stdafx.h"
#include "ConfigurationSettings.h"

int main(int argc, char* argv[])
{
  HRESULT hr = CoInitialize(NULL);

  //Testing <appname.exe>.config Config XML file
  try
  {
    printf(_T("Testing <appname.exe>.config Config XML file....\n"));

    //Constructor without any parameters
    CConfigurationSettings configSettings;
    
    if(configSettings.m_hLastError != S_OK)
      printf(_T("\n\tError: %s"), configSettings.m_lpszLastError);
    else
    {
      TCHAR* lpszConfigValue= NULL;

      //Standard AppSetting
      lpszConfigValue = configSettings.GetAppSetting(_T("pubsConnStr"));
      if(lpszConfigValue)
      {
        printf("\n\tpubsConnStr=>%s", lpszConfigValue);
        delete [] lpszConfigValue; 
        lpszConfigValue = NULL;
      }
      else
      {
        printf(_T("\n\tError: pubsConnStr AppSetting not found!"));
      }

      //Custom XPath Expr
      lpszConfigValue = configSettings.GetConfigValue(_T("/configuration/system.web/compilation/@defaultLanguage"));
      if(lpszConfigValue)
      {
        printf("\n\tdefaultLanguage=>%s", lpszConfigValue);
        delete [] lpszConfigValue; 
        lpszConfigValue = NULL;
      }
      else
      {
        printf(_T("\n\tError: defaultLanguage not found!"));
      }

      printf(_T("\n\tChanging pubsConnStr appSetting to NewValue..."));
      bool bRetVal = configSettings.SetAppSetting(_T("pubsConnStr"), "NewValue");

      printf(_T("\n\tChanging defaultLanguage to C#..."));
      bRetVal = configSettings.SetConfigValue(
        _T("/configuration/system.web/compilation/@defaultLanguage"), 
        "c#");

      printf(_T("\n\tSaving the configuration file..."));
      configSettings.SaveConfigFile();
    }

  }
  catch(...)
  {
    printf("\nException Raised...");
  }

  //  TODO: Test other methods of loading Configuration Files
  //  and namesapaces.

  printf(_T("\n\nPress Enter to continue...\n"));
  getchar();

  CoUninitialize();

  return 0;
}
The above sample application creates an instance of the CConfigurationSettings class. Note that it does not pass any parameters with the constructor and hence for it to work, make sure you have thisapp.exe.config file in the same folder as thisapp.exe. Once the CConfigurationSettings instance is created, the above sample code illustrates using the GetAppSetting, GetConfigValue, SetAppSetting, SetConfigValue, and SaveConfigFile members.

Let's say you have created the above console application as Test.exe and you have a file named test.exe.config as below:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="pubsConnStr" value="SERVER=DEVSERVER;UID=test;PWD=pqr;Database=Pubs;"/>
  </appSettings>
  <system.web>
    <compilation defaultLanguage="vb" debug="true"/>
    <authorization>
      <allow users="DOMAIN\TESTUSERS"/>
    </authorization>
  </system.web>
</configuration>
Now, when you run the above sample application the output would be:



And after this sample application is run, open the .config file and you'll see the new values for defaultLanguage and pubsConnStr configuration settings.

Summary
Once you get some familiarity with MSXML, it is easy to build utility classes as one presented in this article. In this article, you learned how to create a class that uses MSXML DOM, and simplifies working with the XML-based configuration files. The C++ class illustrated in this example supports various ways of loading the XML configuration text, supports namespaces, getting/setting configuration values either by passing the XPath expression or by using a pre-defined location, and finally to persist the changes.
  Contact Us | E-mail Us | Site Guide | About PerfectXML | Advertise ©2004 perfectxml.com. All rights reserved. | Privacy