Introduction
I had a task to write an XML parser to read/write XML files with list of possibilities (requirements).
Requirements:
- Comfort for developer (user of parser)
- Possibility of recording and reading using the same instance of parser
- No use of MFC
- User object writes and reads itself
- Possibility to write in binary stream, without changing source of user objects
- Possibility of creation of proxy classes for reading/writing of user classes, modification of which is forbidden
- Same code in user object for reading and writing
- Possibility of reading/writing simple types
- Possibility of reading/writing objects (with possibility of determination of type during loading)
- Possibility of reading/writing vector, map of simple and complex objects
- Possibility of reading/writing the attributes
- Absences of restrictions on nesting of one type in others
- Minimization of operations "if"
- Unimportance of order of tags in parent tag
- Absence of support namespace and schema
Solution
Based on several ideas:
- Using of parser-a based on sample from Windows Platform SDK
- Each user object that understand that it is his time to be read, throws itself just like "throw this"
- Than parser catch him and put into stack and all calls will be send to this object, until anyone else throw itself or end of object will be found
- Each user object have only one function "determine(ProxyObject& proxy)" it should ovewrite
- Proxy - it is an abstract object that have some virtual functions to read/write some types of data and proxy implementations which are made automaticaly by parser to exactly "know" the type of stream and current operation
- There is a template class with template constructor, that gets members of user objects, proxy object, "name" of the object to read
- This template class understands itself what to do with that members and call corresponding proxy's functions.
- There are some proxy classes that can read/write
string
, int
and float
.
Here is that wonderful template:
template <class T = void*>
class determineMember
{
public:
template <class OT>
determineMember(OT& t,
const char* str,XmlParserProxy& p,T* _t=0) throw(XmlObject*)
{
determine(t,str,p,_t,type(t));
}
};
Currently my parser satisfies all requirements above. And this is example of user objects:
struct Nodes : XmlObject
{
vectorDel<Node*> m_nodes;
virtual void determine(XmlParserProxy& p) throw(XmlObject*)
{
determineMember<StartNode>(m_nodes,"start-node",p);
determineMember<InteractionNode>(m_nodes,"interaction-node",p);
}
};
struct Pipeline : XmlObject
{
string m_name;
int m_count;
float m_sum;
SomeObject* m_someObject;
vector<string> m_transitions;
vector<int> m_point;
Nodes m_nodes;
map<int,NodeDisplay> m_map;
virtual void determine(XmlParserProxy& p) throw(XmlObject*)
{
determineMember<>(m_name,"name",p);
determineMember<>(m_count,"count",p);
determineMember<>(m_sum,"sum",p);
determineMember<FirstObject>(m_someObject,"FirstObject",p);
determineMember<SecondObject>(m_someObject,"SecondObject",p);
determineMember<Nodes>(m_nodes,"nodes",p);
determineMember<string>(m_transitions,"transition",p);
determineMember<int>(m_point,"point",p);
determineMember<>(m_map,"Map",p);
}
};
Note that you can have pointers and vectors of objects and have different classes there.
This is an example of using the parser:
CoInitialize(NULL);
Document doc;
XML::ZXmlParserImpl parser(&doc);
parser.parse(_bstr_t("test.xml"));
parser.save(_bstr_t("saved_test.xml"));
CoUninitialize();
Look at this sample XML file:
<pipeline>
<!-- string -->
<name>Default</name>
<!-- int -->
<count>2</count>
<!-- float -->
<sum>1.234567</sum>
<!-- object having type from set of types-->
<SecondObject>
<second>1</second>
</SecondObject>
<!-- uncomment FirstObject and comment SecondObject
to make FirstObject instead of SecondObject
<FirstObject>
<first>1</first>
</FirstObject>
-->
<!-- new XMLObject of type Nodes-->
<nodes>
<!-- insert in vector<Node> new StartNode,
string attribute-->
<start-node id="Start">
<!-- insert vector<NodeDisplay> new NodeDisplay-->
<node-display>
<x-center>0</x-center>
<y-center>0</y-center>
</node-display>
<node-display>
<x-center>1</x-center>
<y-center>1</y-center>
</node-display>
</start-node>
<!-- insert in vector<Node>
new InteractionNode, string attribute-->
<interaction-node id="It">
<!-- two string attributes-->
<template dynamic="false" index="2">
testS
</template>
</interaction-node>
</nodes>
<!-- vector of strings -->
<transition>Tr1</transition>
<transition>Tr2</transition>
<!-- vector of ints -->
<point>3</point>
<point>2</point>
<point>1</point>
<!-- and now - std::map<int,NodeDisplay> -->
<Map>
<pair>
<name>1</name>
<value>
<x-center>1</x-center>
<y-center>2</y-center>
</value>
</pair>
<pair>
<name>2</name>
<value>
<x-center>4</x-center>
<y-center>3</y-center>
</value>
</pair>
</Map>
</pipeline>
This table shows how to implement determine
function with different members
Member variable |
Source in determine |
Comments |
string m_name; |
determineMember<>(m_name,"name",p); |
|
string m_name; |
determineMember<AtribValue>(m_name,"name",p); |
Use AtribValue to indicate that name is attribute but not a node |
int m_count; |
determineMember<>(m_name,"count",p); |
|
float m_sum; |
determineMember<>(m_name,"sum",p); |
|
SomeObject* m_someObject; |
determineMember <FirstObject>(m_someObject, "FirstObject",p); |
If tag FirstObject found, then new FirstObject will be assigned to m_someObject |
SomeObject* m_someObject; |
determineMember <SecondObject>(m_someObject, "SecondObject",p); |
If tag SecondObject found, then new SecondObjectwill be assigned to m_someObject |
SomeObject m_nodes; |
determineMember<>(m_nodes,"nodes",p); |
|
std::vector<int> m_point; |
determineMember<int>(m_point,"point",p); |
|
std::vector<string> m_transitions; |
determineMember <string>(m_transitions, "transition",p); |
|
std::map<int,SomeObject> m_map; |
determineMember<>(m_map,"Map",p); |
|
std::vector<Node*> m_nodes |
determineMember<StartNode>(m_nodes,"start-node",p); |
If tag start-node found, then new StartNode will be made and inserted in m_nodes |
std::vector<Node*> m_nodes; |
determineMember <InteractionNode>(m_nodes, "interaction-node",p); |
If tag interaction-node found, then new InteractionNode will be made and inserted in m_nodes |
In demo project I have shown how to implement classes for reading/writing std::map
not in parser's source files.