The XML parsing article that should (not) be written!
Table of Contents
- Simple Code Examples
- Introduction
- XML vs. Binary Serialization
- Library Usage
- Element Creation
- Element Removal
- Element Value Assignment
- Element Value Reading
- Namespace
- Enumerating Elements with Same Names
- Enumerating Child Elements with Same Names
- Query Number of Element Children
- Shortcuts to Avoid Temporary Element Creation
- Root Element
- Shared State in Multithreading
- Save File Contents in XML
- Attribute
- Comment
- CData Section
- Base64
- C++0x Move Constructor
- Building in Visual C++ 8.0 (Visual Studio 2005)
- C# Library
- What is in the Elmax Name?
- What's Next?
- Bug Reports
- History
Simple Code Examples
<Books>
<Book>
<Price>12.990000</Price>
</Book>
</Books>
To create the above XML, see the C++ code below:
Elmax::Element root;
root.SetDomDoc(pDoc); // A empty DOM doc is initialized beforehand.
root[L"Books"][L"Book"][L"Price"] = 12.99f;
The third line of code detects that the three elements do not exist, and the float
assignment will attempt to create those three elements and convert 12.99f to string and assign it to the Price element. To read the Price element, we just assign it to the float
variable (see below):
Elmax::Element root;
root.SetDomDoc(pDoc); // An XML file is read into the DOM doc beforehand.
Elmax::Element elemPrice =
root[L"Books"][L"Book"][L"Price"];
if(elemPrice.Exists())
float price = elemPrice;
It is a good practice to check if the Price element exists, using Exists()
, before reading it.
Introduction
Over the years in my profession as a C++ software developer, I had to infrequently maintain XML file formats for application project files. I found the DOM to be difficult to navigate and use. I have come across many articles and XML libraries which proffer to be easy to use, but none is as easy as the internal XML library co-developed by my ex-coworkers, Srikumar Karaikudi Subramanian and Ali Akber Saifee. Srikumar wrote the first version which could only read from an XML file, and Ali later added the node creation capability which allowed the content to be saved in an XML file. However, that library is proprietary. After I left the company, I lost the use of a really-easy-to-use XML library. Unlike many talented programmers out there, I am an idiot; I need an idiot-proof XML library. Too bad, LINQ-to-XML (Xinq) is not available in C++/CLI! I decided to re-construct Srikumar's and Ali's XML library, and made it Open-Source! I dedicate this article to Srikumar Karaikudi Subramanian and Ali Akber Saifee!
My terrible relationship with Ali Akber Saifee
Ali Akber Saifee and I are what we call "the world's greatest arch-rivals". While we worked together in the same company, I would always find every opportunity to find 'flaws' with Ali and email him to expose some of his 'problems' and carbon-copy everyone else. My arch-rival, as always, beat me with some of his best replies. Ali once offered me a chance for us to make good and work together to conquer the world together. But I rejected his offer (in a thinly-veiled plot) to subservient me! The world's greatest arch-rivals can never work together!
Whenever I lost a friend on Facebook, I always check if it was Ali who befriended me. The readers may ask why. Do you, readers, know the ramifications of world's greatest arch-rivals befriending each other on Facebook? Ans: there can never be world peace! Readers may ask why the world's greatest arch-rivals are on each other's Facebook in the first place! Well, that is another story for another article on another day!
Why am I rewriting and promoting my arch-rival's XML library? Before Ali says this, let me pre-empt him and say this myself: Imitation is the most sincere form of flattery. The truth is his XML library is really easy to use!
XML vs. Binary Serialization
In this section, let us look first at the advantages of XML over binary serialization before we discuss Elmax. I'll not discuss XML serialization because I am not familiar with it. Below is the simplified (version 1) file format for an online bookstore:
Version=1
Books
Book*
ISBN
Title
Price
AuthorID
Authors
Author*
Name
AuthorID
The child elements are indented under the parent. The elements which can be more than 1 in quantity are appended with an asterisk(*). The diagram below shows what the (version 1) binary serialization file format will typically look like:
Let's say, in version 2, we add a Description
under Book
and a Biography
under Author
.
Version=2
Books
Book*
ISBN
Title
Price
AuthorID
Description(new)
Authors
Author*
Name
AuthorID
Biography(new)
The diagram below shows versions 1 and 2 of the binary serialization file format. The new additions in version 2 are in lighter colors.
Notice that versions 1 and 2 are binary incompatible? Below is how the binary (note: not binary serialization) file format would choose to implement it:
Version=2
Books
Book*
ISBN
Title
Price
AuthorID
Authors
Author*
Name
AuthorID
Description(new)*
Biography(new)*
This way, version 1 of the application still can read the version 2 binary file while ignoring the new additional parts at the back of the file. If XML is used and without doing any work, version 1 of the application still can read the version 2 XML file (forward compatible) while ignoring the new additional elements, provided that the data types of the original elements remain unchanged and not removed. And the version 2 application can read the version 1 XML file by using the old parsing code (backward compatible). The downside to XML parsing is it is slower than the binary file format, and takes up more space, but XML files are self-describing.
Below is an example of how I would implement the file format in XML, which is followed by a code example to create the XML file:
<?xml version="1.0" encoding="UTF-8"?>
<All>
<Version>2</Version>
<Books>
<Book ISBN="1111-1111-1111">
<Title>How not to program!</Title>
<Price>12.990000</Price>
<Desc>Learn how not to program from the industry`s
worst programmers! Contains lots of code examples
which programmers should avoid! Treat it as reverse
education.</Desc>
<AuthorID>111</AuthorID>
</Book>
<Book ISBN="2222-2222-2222">
<Title>Caught with my pants down</Title>
<Price>10.000000</Price>
<Desc>Novel about extra-martial affairs</Desc>
<AuthorID>111</AuthorID>
</Book>
</Books>
<Authors>
<Author Name="Wong Shao Voon" AuthorID="111">
<Bio>World`s most funny author!</Bio>
</Author>
</Authors>
</All>
The code:
#import <msxml6.dll>
using namespace MSXML2;
HRESULT CTryoutDlg::CreateAndInitDom(
MSXML2::IXMLDOMDocumentPtr& pDoc)
{
HRESULT hr = pDoc.CreateInstance(__uuidof(MSXML2::DOMDocument30));
if (SUCCEEDED(hr))
{
// these methods should not fail so don't inspect result
pDoc->async = VARIANT_FALSE;
pDoc->validateOnParse = VARIANT_FALSE;
pDoc->resolveExternals = VARIANT_FALSE;
MSXML2::IXMLDOMProcessingInstructionPtr pi =
pDoc->createProcessingInstruction
(L"xml", L" version='1.0' encoding='UTF-8'");
pDoc->appendChild(pi);
}
return hr;
}
bool CTryoutDlg::SaveXml(
MSXML2::IXMLDOMDocumentPtr& pDoc,
const std::wstring& strFilename)
{
TCHAR szPath[MAX_PATH];
if(SUCCEEDED(SHGetFolderPath(NULL,
CSIDL_LOCAL_APPDATA|CSIDL_FLAG_CREATE,
NULL,
0,
szPath)))
{
PathAppend(szPath, strFilename.c_str());
}
variant_t varFile(szPath);
return SUCCEEDED(pDoc->save(varFile));
}
void CTryoutDlg::TestWrite()
{
MSXML2::IXMLDOMDocumentPtr pDoc;
HRESULT hr = CreateAndInitDom(pDoc);
if (SUCCEEDED(hr))
{
using namespace Elmax;
using namespace std;
Element root;
root.SetConverter(NORMAL_CONV);
root.SetDomDoc(pDoc);
Element all = root[L"All"];
all[L"Version"] = 2;
Element books = all[L"Books"].CreateNew();
Element book1 = books[L"Book"].CreateNew();
book1.Attribute(L"ISBN") = L"1111-1111-1111";
book1[L"Title"] = L"How not to program!";
book1[L"Price"] = 12.99f;
book1[L"Desc"] = L"Learn how not to program from the industry`s
worst programmers! Contains lots of code examples which
programmers should avoid! Treat it as reverse education.";
book1[L"AuthorID"] = 111;
Element book2 = books[L"Book"].CreateNew();
book2.Attribute(L"ISBN") = L"2222-2222-2222";
book2[L"Title"] = L"Caught with my pants down";
book2[L"Price"] = 10.00f;
book2[L"Desc"] = L"Novel about extra-martial affairs";
book2[L"AuthorID"] = 111;
Element authors = all[L"Authors"].CreateNew();
Element author = authors[L"Author"].CreateNew();
author.Attribute(L"Name") = L"Wong Shao Voon";
author.Attribute(L"AuthorID") = 111;
author[L"Bio"] = L"World`s most funny author!";
std::wstring strFilename = L"Books.xml";
SaveXml(pDoc, strFilename);
}
}
Here is the code to read the XML which was saved in the previous code snippet. Some helper class (DebugPrint
) and methods (CreateAndLoadXml
and DeleteFile
) are omitted to focus on the relevant code. The helper class and methods can be found in the Tryout project in the source code download.
void CTryoutDlg::TestRead()
{
DebugPrint dp;
MSXML2::IXMLDOMDocumentPtr pDoc;
std::wstring strFilename = L"Books.xml";
HRESULT hr = CreateAndLoadXml(pDoc, strFilename);
if (SUCCEEDED(hr))
{
using namespace Elmax;
using namespace std;
Element root;
root.SetConverter(NORMAL_CONV);
root.SetDomDoc(pDoc);
Element all = root[L"All"];
if(all.Exists()==false)
{
dp.Print(L"Error: root does not exists!");
return;
}
dp.Print(L"Version : {0}\n\n",
all[L"Version"].GetInt32(0));
dp.Print(L"Books\n");
dp.Print(L"=====\n");
Element books = all[L"Books"];
if(books.Exists())
{
Element::collection_t vecBooks =
books.GetCollection(L"Book");
for(size_t i=0; i<vecBooks.size(); ++i)
{
dp.Print(L"ISBN: {0}\n",
vecBooks[i].Attribute(L"ISBN").GetString(L"Error"));
dp.Print(L"Title: {0}\n",
vecBooks[i][L"Title"].GetString(L"Error"));
dp.Print(L"Price: {0}\n",
vecBooks[i][L"Price"].GetFloat(0.0f));
dp.Print(L"Desc: {0}\n",
vecBooks[i][L"Desc"].GetString(L"Error"));
dp.Print(L"AuthorID: {0}\n\n",
vecBooks[i][L"AuthorID"].GetInt32(-1));
}
}
dp.Print(L"Authors\n");
dp.Print(L"=======\n");
Element authors = all[L"Authors"];
if(authors.Exists())
{
Element::collection_t vecAuthors =
authors.GetCollection(L"Author");
for(size_t i=0; i<vecAuthors.size(); ++i)
{
dp.Print(L"Name: {0}\n",
vecAuthors[i].Attribute(L"Name").GetString(L"Error"));
dp.Print(L"AuthorID: {0}\n",
vecAuthors[i].Attribute(L"AuthorID").GetInt32(-1));
dp.Print(L"Bio: {0}\n\n",
vecAuthors[i][L"Bio"].GetString(L"Error"));
}
}
}
DeleteFile(strFilename);
}
This is the output after the XML is read:
Version : 2
Books
=====
ISBN: 1111-1111-1111
Title: How not to program!
Price: 12.990000
Desc: Learn how not to program from the industry`s
worst programmers! Contains lots of code examples
which programmers should avoid! Treat it as reverse education.
AuthorID: 111
ISBN: 2222-2222-2222
Title: Caught with my pants down
Price: 10.000000
Desc: Novel about extra-martial affairs
AuthorID: 111
Authors
=======
Name: Wong Shao Voon
AuthorID: 111
Bio: World`s most funny author!
Library Usage
In the section, we'll look at how to use the Elmax library to perform creation, reading, update, and deletion (CRUD) on elements, attributes, CData sections, and comments. As you can see from the previous code sample, Elmax makes use of the Microsoft XML DOM library. That's because I do not wish to re-create all that XML functionality, for instance, XPath. Since Elmax depends on Microsoft XML, which in turn depends on COM, to work, we have to call CoInitialize(NULL);
to initialize the COM runtime at the start of the application, and also call CoUninitialize();
to uninitialize it before the application ends. Elmax is an abstraction over DOM; however, it does not seek to replicate all the functionality of DOM. For example, the programmer cannot use Elmax to read element siblings. In the Elmax model, an element is a first class citizen. Attribute, CData section, and comment are children of an element! This is different from DOM, where they are nodes in their own right. The reason I designed CData section and comment to be children of element, is because CData section and comment are not identifiable by a name or ID.
Element Creation
Element all = root[L"All"];
all[L"Version"] = 1;
Element books = all[L"Books"].CreateNew();
Typically, we use CreateNew
to create elements. There is also a Creat
e method. The difference is the Create
method will not create elements if they already exist. Notice that I did not use Create
or CreateNew
to create All
and Version
elements. That's because they are created automatically when I assign a value to the last element on the chain. Note that when you call CreateNew
repeatedly, only the last element gets created. Let me show you a code example to explain this.
root[L"aa"][L"bb"][L"cc"].CreateNew();
root[L"aa"][L"bb"][L"cc"].CreateNew();
root[L"aa"][L"bb"][L"cc"].CreateNew();
In the first CreateNew
call, elements aa
, bb
, and cc
are created. In each subsequent call, only the element cc
is created. This is the resultant XML created (and indented for easy reading):
<aa>
<bb>
<cc/>
<cc/>
<cc/>
</bb>
</aa>
Create
and CreateNew
have an optional std::wstring
parameter to specify the namespace URI. If your element belongs to a namespace, then you must create it explicitly, using Create
or CreateNew
; this means you cannot rely on value assignment to create it automatically. More on this later. Note: when calling instance Element methods other than Create
, CreateNew
, setters, and accessors when the element(s) does not exist, Elmax will raise an exception! When do we use Create
instead of CreateNew
? One possible scenario is when the application loads an XML file, edits it, and saves it. In the edit stage, it does not check if an element exists in the original XML file before assigning it or adding nodes: call Create
which will create it if it does not exist, otherwise Create
does nothing.
Element Removal
using namespace Elmax;
Element elem;
Element elemChild = elem[L"Child"];
// do processing
elem.RemoveNode(elemChild); // Remove its child node.
elem.RemoveNode(); // Remove itself from DOM.
Note: for the AddNode
method, you can only add a node which has been removed in the current version.
Element Value Assignment
In the beginning of the article, I showed how to create elements and assign a value to the last element at the same time. I'll repeat that code snippet here:
Elmax::Element root;
root.SetDomDoc(pDoc); // A empty DOM doc is initialized beforehand.
root[L"Books"][L"Book"][L"Price"] = 12.99f;
It turns out that this example is dangerous as it uses an overloaded assignment operator determined by the compiler. What if you mean to assign a float but assigns an integer instead just because you forgot to add a .0
and append a f
to the float value? Not much harm in this case, I suppose. In all scenarios, it is better to use the setter method to assign a value explicitly.
Elmax::Element root;
root.SetDomDoc(pDoc); // A empty DOM doc is initialized beforehand.
root[L"Books"][L"Book"][L"Price"].SetFloat(12.99f);
Here is the list of setter methods available:
bool SetBool(bool val);
bool SetChar(char val);
bool SetShort(short val);
bool SetInt32(int val);
bool SetInt64(__int64 val);
bool SetUChar(unsigned char val);
bool SetUShort(unsigned short val);
bool SetUInt32(unsigned int val);
bool SetUInt64(unsigned __int64 val);
bool SetFloat(float val);
bool SetDouble(double val);
bool SetString(const std::wstring& val);
bool SetString(const std::string& val);
bool SetGUID(const GUID& val);
bool SetDate(const Elmax::Date& val);
bool SetDateTime(const Elmax::DateAndTime& val);
Element Value Reading
In the beginning of the article, I showed how to read a value from an element. I'll repeat the code snippet here:
Elmax::Element root;
root.SetDomDoc(pDoc); // A XML file is read into the DOM doc beforehand.
Elmax::Element elemPrice = root[L"Books"][L"Book"][L"Price"];
if(elemPrice.Exists())
float price = elemPrice;
This is the more correct version, using the GetFloat
accessor to specify a default value.
Elmax::Element root;
root.SetDomDoc(pDoc); // A XML file is read into the DOM doc beforehand.
Elmax::Element elemPrice = root[L"Books"][L"Book"][L"Price"];
if(elemPrice.Exists())
float price = elemPrice.GetFloat(10.0f);
Price will get a default value of 10.0f if the value does not exist or is invalid, whereas the prior example before this, will get a 0.0f
because a default value is not specified. But by default, Elmax does not know the string value is an improper float value in textual form, unless you use a Regular Expression to validate the string value. Set REGEX_CONV
instead of NORMAL_CONV
in the root element to use the Regular Expression type converter. As an alternative, you can use a schema or DTD to validate your XML before doing Elmax parsing. To learn about schema or DTD validation, please consult MSDN.
Elmax::Element root;
root.SetConverter(REGEX_CONV);
This is the declaration of the SetConverter
method:
//! Set the type converter pointer
void SetConverter(CONVERTER conv, IConverter* pConv=NULL);
To use your own custom type converter, set the optional pConv
pointer.
Elmax::Element root;
root.SetConverter(CUSTOM_CONV, pCustomTypeConv);
You are responsible for the deletion of pCustomTypeConv
if it is allocated on the heap. There are locale type converters in Elmax, but they are not tested at this point because I am not sure how to test them, as in Asia, number representation is the same in all countries, unlike in Europe. As a tip to readers who might be modifying Elmax, remember to run through all the 255 unit tests to make sure you did not break anything after a modification. The unit test is only available for running in Visual Studio 2010. Below is a list of value accessors available:
bool GetBool(bool defaultVal) const;
char GetChar(char defaultVal) const;
short GetShort(short defaultVal) const;
int GetInt32(int defaultVal) const;
__int64 GetInt64(__int64 defaultVal) const;
unsigned char GetUChar(unsigned char defaultVal) const;
unsigned short GetUShort(unsigned short defaultVal) const;
unsigned int GetUInt32(unsigned int defaultVal) const;
unsigned __int64 GetUInt64(unsigned __int64 defaultVal) const;
float GetFloat(float defaultVal) const;
double GetDouble(double defaultVal) const;
std::wstring GetString(const std::wstring& defaultVal) const;
std::string GetString(const std::string& defaultVal) const;
GUID GetGUID(const GUID& defaultVal) const;
Elmax::Date GetDate(const Elmax::Date& defaultVal) const;
Elmax::DateAndTime GetDateTime(const Elmax::DateAndTime& defaultVal) const;
For GetBool
and the interpretation of a boolean value, true
, yes
, ok
, and 1
evaluate to be true while false
, no
, cancel
, and 0
evaluate to be false. They are not case-sensitive.
Namespace
To create an element under a namespace URI "http:www.yahoo.com", see below:
using namespace Elmax;
Element all = root[L"All"];
all[L"Version"] = 1;
Element books = all[L"Books"].CreateNew();
Element book1 = books[L"Book"].CreateNew(L"http://www.yahoo.com");
The XML output is as below:
<?xml version="1.0" encoding="UTF-8"?>
<All>
<Version>1</Version>
<Books>
<Book xmlns="http://www.yahoo.com"/>
</Books>
</All>
To create a bunch of elements and attributes under a namespace URI, see below:
using namespace Elmax;
Element all = root[L"All"];
all[L"Version"] = 1;
Element books = all[L"Books"].CreateNew();
Element book1 = books[L"Yahoo:Book"].CreateNew(L"http://www.yahoo.com");
book1.Attribute(L"Yahoo:ISBN").Create(L"http://www.yahoo.com");
book1.Attribute(L"Yahoo:ISBN") = L"1111-1111-1111";
book1[L"Yahoo:Title"].Create(L"http://www.yahoo.com");
book1[L"Yahoo:Title"] = L"How not to program!";
book1[L"Yahoo:Price"].Create(L"http://www.yahoo.com");
book1[L"Yahoo:Price"] = 12.99f;
book1[L"Yahoo:Desc"].Create(L"http://www.yahoo.com");
book1[L"Yahoo:Desc"] = L"Learn how not to program from the industry`s
worst programmers! Contains lots of code examples which programmers
should avoid! Treat it as reverse education.";
book1[L"Yahoo:AuthorID"].Create(L"http://www.yahoo.com");
book1[L"Yahoo:AuthorID"] = 111;
The XML output is as below:
<All>
<Version>1</Version>
<Books>
<Yahoo:Book xmlns:Yahoo="http://www.yahoo.com"
Yahoo:ISBN="1111-1111-1111">
<Yahoo:Title>How not to program!</Yahoo:Title>
<Yahoo:Price>12.990000</Yahoo:Price>
<Yahoo:Desc>Learn how not to program from the
industry`s worst programmers! Contains lots of code
examples which programmers should avoid! Treat it
as reverse education.</Yahoo:Desc>
<Yahoo:AuthorID>111</Yahoo:AuthorID>
</Yahoo:Book>
</Books>
</All>
Enumerating Elements with Same Names
You can use the AsCollection
method to return siblings with the same name in a vector
.
using namespace Elmax;
Element root;
root.SetConverter(NORMAL_CONV);
root.SetDomDoc(pDoc);
Element elem1 = root[L"aa|bb|cc"].CreateNew();
elem1.SetInt32(11);
Element elem2 = root[L"aa|bb|cc"].CreateNew();
elem2.SetInt32(22);
Element elem3 = root[L"aa|bb|cc"].CreateNew();
elem3.SetInt32(33);
Element::collection_t vec = root[L"aa"][L"bb"][L"cc"].AsCollection();
for(size_t i=0;i<vec.size(); ++i)
{
int n = vec.at(i).GetInt32(10);
}
This overloaded form (below) of AsCollection
is faster as it does not create a temporary vector
before returning.
typedef std::vector< Element > collection_t;
bool AsCollection(const std::wstring& name, collection_t& vec);
Enumerating Child Elements with Same Names
You can use the GetCollection
method to get children with the same names in a vector
.
using namespace Elmax;
Element root;
root.SetConverter(NORMAL_CONV);
root.SetDomDoc(pDoc);
Element elem1 = root[L"aa|bb|cc"].CreateNew();
elem1.SetInt32(11);
Element elem2 = root[L"aa|bb|cc"].CreateNew();
elem2.SetInt32(22);
Element elem3 = root[L"aa|bb|cc"].CreateNew();
elem3.SetInt32(33);
Element::collection_t vec = root[L"aa"][L"bb"].GetCollection(L"cc");
for(size_t i=0;i<vec.size(); ++i)
{
int n = vec.at(i).GetInt32(10);
}
This overloaded form (below) of GetCollection
is faster as it does not create a temporary vector
before returning.
typedef std::vector< Element > collection_t;
bool GetCollection(const std::wstring& name, collection_t& vec);
Query Number of Element Children
To query the number of children for each name, you can use the QueryChildrenNum
method.
using namespace Elmax;
Element root;
root.SetConverter(NORMAL_CONV);
root.SetDomDoc(pDoc);
Element elem1 = root[L"aa|bb|qq"].CreateNew();
elem1.SetInt32(11);
Element elem2 = root[L"aa|bb|cc"].CreateNew();
elem2.SetInt32(22);
Element elem3 = root[L"aa|bb|cc"].CreateNew();
elem3.SetInt32(33);
Element elem4 = root[L"aa|bb|qq"].CreateNew();
elem4.SetInt32(44);
Element elem5 = root[L"aa|bb|cc"].CreateNew();
elem5.SetInt32(55);
Element::available_child_t acmap =
root[L"aa"][L"bb"].QueryChildrenNum();
assert(acmap[L"cc"] == (unsigned int)(3));
assert(acmap[L"qq"] == (unsigned int)(2));
There is also an overloaded form (below) of QueryChildrenNum
which does not create a temporary vector
before returning. Note: QueryChildrenNum
can only query for elements, not attributes or CData sections or comments.
typedef std::map< std::wstring, size_t > available_child_t;
bool QueryChildrenNum(available_child_t& children);
Shortcuts to Avoid Temporary Element Creation
In the previous enumeration example, I used:
Elmax::Element elem1 = root[L"aa|bb|cc"].CreateNew();
instead of:
Elmax::Element elem1 = root[L"aa"][L"bb"][L"cc"].CreateNew();
because the second form creates temporary elements aa
and bb
on the stack which are not used. The first form saves some tedious typing, and only returns an element in the overloaded []
operator, not to say it is faster too. \\
and /
can be used for delimiters as well. To speed up, use the below code which excessively uses temporaries:
if(root[L"aa"][L"bb"][L"cc"][L"dd"].Exists())
{
root[L"aa"][L"bb"][L"cc"][L"dd"][L"Title"] = L"Beer jokes";
root[L"aa"][L"bb"][L"cc"][L"dd"][L"Author"] = L"The joker";
root[L"aa"][L"bb"][L"cc"][L"dd"][L"Price"] = 10.0f;
}
You can assign it to a Element
variable, and use that variable instead.
Elmax::Element elem1 = root[L"aa|bb|cc|dd"];
if(elem1.Exists())
{
elem1[L"Title"] = L"Beer jokes";
elem1[L"Author"] = L"The joker";
elem1[L"Price"] = 10.0f;
}
Root Element
The root element is created when you call SetDomDoc
on the element. You should know by now that the []
operator is used to access the child element. For root elements, the []
operator accesses itself to see its name corresponds to the name in the []
operator.
Element root;
root.SetDomDoc(pDoc);
Element elem1 = root[L"aa|bb|cc"];
The aa
element in the above example actually refers to the root, not the child of the root. If an element is not called with SetDomDoc
, then aa
refers to its child. When using the []
operator, please remember to prefix the (wide) string literal with L
, e.g., elem[L"Hello"]
, else you will get a strange unhelpful error. Element objects are created directly or indirectly from the root. For example, the root creates the aa
element, and the aa
element has the ability to create other elements. If you instantiate your element not from the root, your element cannot create. This is the limitation of the MS XML DOM in which only the DOM document creates nodes. Those elements which are created directly or indirectly from the root received their DOM document, thus the ability to create elements.
Shared State in Multithreading
You might be using different Elmax Element
objects in different threads without sharing them across threads. However, Element
has static type converter objects which are shared with all Element
objects. To overcome this problem, allocate a new type converter and use that in the root. Remember to delete the converter after use.
using namespace Elmax;
Element root;
root.SetDomDoc(pDoc);
RegexConverter* pRegex = new RegexConverter();
root.SetConverter(CUSTOM_CONV, pRegex);
By the way, you need to remember to call CoInitialize/CoUninitialize
in your worker threads!
Save File Contents in XML
You can call SetFileContents
to save a file's binary contents in Base64 format in the Element
. You can specify to save its file name and file length in the attributes if you intended to save the contents back to a file with the same name on disk. We need to save the original file length as well because GetFileContents
sometimes reports a longer length after the Base64 conversion!
bool SetFileContents(const std::wstring& filepath,
bool bSaveFilename, bool bSaveFileLength);
We use GetFileContents
to get back the file content from Base64 conversion. filename
is written, provided that you did specify to save the file name during SetFileContents
. length
is the length of the returned character array, not the saved file length attribute.
char* GetFileContents(std::wstring& filename, size_t& length);
Attribute
To create an attribute (if not exists) and assign a string to it, see example below:
book1.Attribute(L"ISBN") = L"1111-1111-1111";
To create an attribute with a namespace URI and assign a string to it, you have to create it explicitly.
book1.Attribute(L"Yahoo:ISBN").Create(L"http://www.yahoo.com");
book1.Attribute(L"Yahoo:ISBN") = L"1111-1111-1111";
To delete an attribute, use the Delete
method.
book1.Attribute(L"ISBN").Delete();
To find out an attribute with the name exists, use the Exists
method.
bool bExists = book1.Attribute(L"ISBN").Exists();
The list of Attribute
setters and accessors are the same as that for Element
. And they use the same type converter.
Comment
For your information, XML comments come in the form of <!--My example comments here-->
. Below are a bunch of operations you can use with comments:
using namespace Elmax;
Element elem = root[L"aa"][L"bb"][L"cc"].CreateNew();
elem.AddComment(L"Can you see me?"); // add a new comment!
Comment comment = elem.GetComment(0); // get comment at 0 index
comment.Update(L"Can you hear me?"); // update the comment
comment.Delete(); // Delete this comment node!
You can get a vector of Comment
objects which are children of the element, using the GetCommentCollection
method.
CData Section
For your information, an XML CData section comes in the form of <![CDATA[<IgnoredInCDataSection/>]]>
. An XML CData section typically contains data which is not parsed by the parsers, therefore it can contain < and > and other invalid text characters. Some programmers prefer to store them in Base64 format (see next section). Below are a bunch of operations you can use with CData sections:
using namespace Elmax;
Element elem = root[L"aa"][L"bb"][L"cc"].CreateNew();
elem.AddCData(L"<<>>"); // add a new CData section!
CData cdata = elem.GetCData(0); // get CData section at 0 index
cdata.Update(L">><<"); // update the CData section
cdata.Delete(); // Delete this CData section node!
You can get a vector of CData
sections which are children of the element, using the GetCDataCollection
method.
Base64
Some programmers prefer to store binary data in Base64 format under an element, instead of in the CData
section, to easily identify and find it. The downside is Base64 format takes up more space, and data conversion takes time. This code example shows how to use Base64 conversion before assignment, and also convert back from Base64 to binary data after reading:
Elmax::Element elem1;
string strNormal = "@#$^*_+-|\~<>";
// Assigning base64 data
elem1 = Element::ConvToBase64(strNormal.c_str(), strNormal.length());
// Reading base64 data
wstring strBase64 = elem1.GetString(L"ABC");
size_t len = 0;
// Get the length required
Element::ConvFromBase64(strBase64, NULL, len);
char* p = new char[len+1];
memset(p, 0, len+1);
Element::ConvFromBase64(strBase64, p, len);
// process p here (not shown)(Remember to delete p).
C++0x Move Constructor
The Elmax library defines some C++0x move constructors and move assignments. In order to build the library in older Visual Studio versions prior to the 2010 version, you have to hide them by defining _HAS_CPP0X
to be 0
in stdafx.h.
Building in Visual C++ 8.0 (Visual Studio 2005)
Elmax is available to be built in Visual Studio 2005/2008/2010. However, additional effort is required to build the Boost Regular Expression library for Visual Studio 2005. I did not realise how difficult it is for a user to build Elmax in Visual C++ 8.0 until my computer broke down two weeks ago; the old computer has Boost regex libraries prebuilt while the new computer doesn't. To get the regex library for your computer, you need to download Boost and add Boost the regex include folder path to the Visual Studio VC++ directories and build the Boost regex libraries, according to this Boost documentation. For Visual Studio 2008/2010 users, Elmax will use the C++0x TR1's regex library.
C# Library
Elmax has been ported to C# 2.0 and 87 unit tests have been added to test the C# library, this brings the total number of unit tests to 342. The C# library can be built in Visual Studio 2005/2008/2010. There are plans to port Elmax to C# 4.0 to take advantage of optional parameters. It is interesting to note that the C++ version has approximately 7365 lines of code while the C# version has 2622 lines! The C++ version has 10 classes while C# has 4 classes because the helper classes are already implemented in the .NET BCL libraries, so there is no need for me to write them. Because there are less classes, less unit tests are needed to test the C# version (87 versus 255). Just comparing Element.cpp and Element.cs, Element.cpp has 2200 lines of code while Element.cs has 1755 lines! Below is the code to write an element in C++ followed by the C# version. You can see that they are almost similar. However, the C# version of Elmax does not have the implicit assignment and reading of value types. Assignment and reading of elements has to be done through setters and accessors, for example, the SetFloat
and GetFloat
methods.
// C++ version of writing element
using namespace Elmax;
...
Element root;
root.SetDomDoc(pDoc); // A empty DOM doc is initialized beforehand.
root[L"Books"][L"Book"][L"Price"].SetFloat(12.99f);
// C# version of writing element
using Elmax;
...
Element root = new Element();
root.SetDomDoc(doc); // A empty DOM doc is initialized beforehand.
root["Books"]["Book"]["Price"].SetFloat(12.99f);
Below is the code to read an element in C++ followed by the C# version:
// C++ version of reading element
using namespace Elmax;
...
Element root;
root.SetDomDoc(pDoc); // A XML file is read into the DOM doc beforehand.
Element elemPrice = root[L"Books"][L"Book"][L"Price"];
if(elemPrice.Exists())
float price = elemPrice.GetFloat(10.0f);
// C# version of reading element
using Elmax;
...
Element root = new Element();
root.SetDomDoc(doc); // A XML file is read into the DOM doc beforehand.
Element elemPrice = root["Books"]["Book"]["Price"];
if(elemPrice.Exists)
float price = elemPrice.GetFloat(10.0f);
What is in the Elmax Name?
The abstraction model and the library is named "Elmax" because there is an 'X', 'M', and 'L' in "Elmax". <soft whisper> I can tell you the real reason, but you must not tell anyone, else I have to eliminate you from this world! The reason is the author likes to crack jokes in real life. But all his jokes are deemed by everyone to be lame and cold. In the Chinese language, a cold joke means a joke which is not funny or laughable at all! If you rearrange alphabets in "Elmax", you get "LameX", which refers to the author!</soft whisper>.
What's Next?
In the next article, the XML parsing is going to get even easier! That is, parsing is eliminated; the programmer does not have to do the XML parsing himself/herself! XML parsing is done automatically, along the lines of Object Relational Mapping (ORM). I personally don't see the need for a programmer to do XML parsing. Just pass in a specially formatted structure(s) with an XML file and the library will fill in the structure for you! Just think that I am kidding! There is no way I'll have time for this as my part-time undergrad course is starting soon!
Thanks for reading!
Bug Reports
For bug reports and feature requests, please file them here. When you file a bug report, please do include the sample code and XML file (if any) to reproduce the bug. The current Elmax is at version 0.65 beta. The Elmax CodePlex site is located at http://elmax.codeplex.com/.
History
- 11/02/2011: Ported the library to C#. Added Visual Studio 2008 solution and projects. Added an article section on how to build Elmax in Visual Studio 2005.
- 10/01/2011: Updated the source code (version 0.65 beta) to use ATL Base64 implementation, and changed the getter and setter methods not to raise exceptions because of null type converter pointer, by using a valid static type converter object.
- 26/12/2010: Added a
TestRead
code snippet to show how to read an XML file. Updated the source code. - 24/12/2010: Updated the VC2005 code to use Boost regex, instead of the bundled TR1 regex in newer VC. Added ability to access Date, GUID, and file contents in XML.
- 23/12/2010: First release.
Post Comment
IJtpT3 Really enjoyed this article post.Much thanks again. Keep writing.