|
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.