- Platforms and License
- Technologies Supported
- Where to Obtain It
- Reusable Design and Code: DOM Walkers
- Implementing DOMWalkerWizard
- About This Article
Reusable Design and Code: DOM Walkers
One of the main techniques for DOM programming is the use of Visitors (also known as Walkers), which iterate through the document, as a collection of DOM nodes, performing a particular manipulation.
Figure 1 shows the structure of our Walker classes, as implemented in the following examples.
Figure 1 DOMWalker Class Diagram
Most of the aspects about Walkers are instances of reusable design as much as they are of reusable code. After all, a DOM Walker will always perform the following implementable actions:
Check the current node for its type, and decide what to do accordingly
Decide whether it wants to continue exploring the tree
Decide and implement how to explore the tree (implement a particular form of traversal: preorder, inorder, postorder, and so on)
Also, for each Walker (I use the terms Walker and Visitor interchangeably in this section), there is a client, supplying the DOM node from which the traversal begins. This client must be implemented repeatedly every time a Walker is constructed in order to do unit tests.
By this time, you no doubt have guessed the nature of our next Framework: the automatic creation of DOM Walkers using MSXML3.
A Particular Walker: BlockWorldRenderer
Before jumping into the creation of the Framework (implemented as a Visual Studio Project Wizard), take a look at an example of the type of application you want to generate.
Listing 1 shows the DTD for defining 3D figures in a world made of blocks. A block is identified by its position (three integers, one for each coordinate) and a color. Groups of blocks can be rotated arbitrarily, encapsulating them in a rotation element.
Your 3D worlds may also have additional information regarding the author of the model, and other additional data that will be ignored by the rendering engine. (You will create a DOM Walker named BlockWorldRenderer.)
Listing 1: BlockWorld.dtd
<!ELEMENT blockworld (head?,world)> <!ELEMENT head (author?,name,version?)> <!ELEMENT author (#PCDATA)> <!ELEMENT name (#PCDATA)> <!ELEMENT version EMPTY> <!ATTLIST version release CDATA #REQUIRED build CDATA #REQUIRED> <!ELEMENT world (rotation|block)*> <!ENTITY % color "white|red|blue|green|purple|black"> <!ELEMENT block EMPTY> <!ATTLIST block x CDATA #REQUIRED y CDATA #REQUIRED z CDATA #REQUIRED color (%color;) #REQUIRED> <!ELEMENT rotation (rotation|block)*> <!ATTLIST rotation angle CDATA #REQUIRED axis (x|y|z) #REQUIRED>
A simple blockworld, a 3D cross, is defined in Listing 2, using an XML document conformant with Listing 1.
Listing 2: 3DCross.xml
<?xml version="1.0"?> <!DOCTYPE blockworld SYSTEM "blockworld.dtd"> <blockworld> <head> <name>A 3D Cross</name> <version release="1" build="0"/> </head> <world> <block x="0" y="1" z="0" color="red"/> <block x="0" y="0" z="0" color="red"/> <block x="0" y="-1" z="0" color="red"/> <block x="1" y="0" z="0" color="red"/> <block x="-1" y="0" z="0" color="red"/> <block x="0" y="0" z="-1" color="red"/> <block x="0" y="0" z="1" color="red"/> </world> </blockworld>
The result of processing Listing 2 must be the display of the 3D blocks in a screen. For this purpose, use OpenGL, which is the industry library standard for 3D graphics. You will program a Walker that will go through the DOM tree, generating the appropriate OpenGL function calls as it encounters block elements.
Figure 2 shows the final result that you want to achieve (with the exception that in the program the cross is constantly rotating).
Figure 2 BlockWorld C++ Running
The BlockWorldDOMWalker Class
The BlockWorldDOMWalker class is a typical visitor, which goes over a collection of nodes by analyzing them one at the time and then calling itself recursively for each child node.
Listing 3 shows the implementation of the class. Note how a helper class has been defined and is used to read the file from disk into an IXMLDOMNodePtr (in other words, MSXML3 pointer to a DOM Node). A simple inspection reveals that the structure and principles of msxml3 DOM manipulation are the same as those used previously with Xerces, but some names vary. In order to get familiar with the naming conventions, I recommend you read the help file associated with msxml3. (It gets installed under "\Program Files\Microsoft XML Parser SDK\docs\xmlsdk20.chm".)
Listing 3: BlockWorldWalker
include "BlockWorldXMLWalker.h" /** recursive method used to navigate the tree */ void BlockWorldXMLWalker::visit(IXMLDOMNodePtr n) { if(n->nodeType == NODE_ELEMENT) { if(!strcmp(n->nodeName,"block")) { // Get the values for each coordinate IXMLDOMNodePtr x,y,z,strColor; color cubeColor = black; n->attributes->get_item(0,&x); n->attributes->get_item(1,&y); n->attributes->get_item(2,&z); n->attributes->get_item(3,&strColor); if(!strcmp(strColor->text,"red")) cubeColor = red; if(!strcmp(strColor->text,"white")) cubeColor = white; if(!strcmp(strColor->text,"green")) cubeColor = green; if(!strcmp(strColor->text,"blue")) cubeColor = blue; if(!strcmp(strColor->text,"purple")) cubeColor = purple; putCube((int)atoi(x->text), //x,y, and z are bstrs. // They are converted to ints (int)atoi(y->text), (int)atoi(z->text), cubeColor); return; // No interest in looking at the // children of a block (none!) } else if(!strcmp(n->nodeName,"rotation")) { // Get the values for each coordinate IXMLDOMNodePtr axis,angle; n->attributes->get_item(0,&angle); n->attributes->get_item(1,&axis); if(!strcmp(axis->text,"x")) glRotatef((int)atoi(angle->text), 1.0f, 0.0f, 0.0f); else if(!strcmp(axis->text,"y")) glRotatef((int)atoi(angle->text), 0.0f, 1.0f, 0.0f); else glRotatef((int)atoi(angle->text), 0.0f, 0.0f, 0.0f); // Now traverse the rest of the tree, //looking for more nodes or rotations for(int i = 0; i < n->childNodes->length ;i++) visit(n->childNodes->item[i]); // Now put us back where we started if(!strcmp(axis->text,"x")) glRotatef(-(int)atoi(angle->text), 1.0f, 0.0f, 0.0f); else if(!strcmp(axis->text,"y")) glRotatef(-(int)atoi(angle->text), 0.0f, 1.0f, 0.0f); else glRotatef(-(int)atoi(angle->text), 0.0f, 0.0f, 0.0f); return; // Already processed its children! } } // Now traverse the rest of the tree, looking for more nodes or rotations for(int i = 0; i < n->childNodes->length ;i++) visit(n->childNodes->item[i]); } IXMLDOMNodePtr BlockWorldXMLWalker::Helper::readXMLFile(const char *name) { try { IXMLDOMDocumentPtr docPtr; //init docPtr.CreateInstance("msxml2.domdocument"); // load a document _variant_t varXml(name); _variant_t varOut((bool)TRUE); varOut = docPtr->load(varXml); if ((bool)varOut == FALSE) throw(0); return docPtr; // remember, a document Node is also a Node } catch(...) { MessageBox(NULL, "Exception occurred! Malformed or nonexistent file", "Error", MB_OK); } return NULL; }
Even when it is not necessary to know OpenGL to see the XML points of this example, Listing 4 shows the OpenGL code, just in case you are curious about it.
Listing 4: putCube - OpenGL Related Functions
/** put a cube with color col in a particular point of the world(x,y,z) Included here for readability purposes */ void putCube(int x,int y,int z,enum color col) { switch(col) { case black: glColor3f(BLACK); break; case red: glColor3f(RED); break; case blue: glColor3f(BLUE); break; case green: glColor3f(GREEN); break; case purple: glColor3f(PURPLE); break; default: glColor3f(WHITE); break; } glTranslatef((float)x*2,(float)y*2,(float)z*2); // Draw the sides of the cube glBegin(GL_QUAD_STRIP); glNormal3d(0.0, 0.0, -1.0); // Normal A glVertex3d(1, 1, -1); // Vertex 1 glVertex3d(1, -1, -1); // Vertex 2 glVertex3d(-1, 1, -1); // Vertex 3 glVertex3d(-1, -1, -1); // Vertex 4 glNormal3d(-1.0, 0.0, 0.0); // Normal B glVertex3d(-1, 1, 1); // Vertex 5 glVertex3d(-1, -1, 1); // Vertex 6 glNormal3d(0.0, 0.0, 1.0); // Normal C glVertex3d(1, 1, 1); // Vertex 7 glVertex3d(1, -1, 1); // Vertex 8 glNormal3d(1.0, 0.0, 0.0); // Normal D glVertex3d(1, 1, -1); // Vertex 9 glVertex3d(1, -1, -1); // Vertex 10 glEnd(); // Draw the top and bottom of the cube glBegin(GL_QUADS); glNormal3d(0.0, 1.0, 0.0); glVertex3d(-1, -1, -1); glVertex3d(1, -1, -1); glVertex3d(1, -1, 1); glVertex3d(-1, -1, 1); glNormal3d(0.0, 1.0, 0.0); glVertex3d(-1, 1, -1); glVertex3d(1, 1, -1); glVertex3d(1, 1, 1); glVertex3d(-1, 1, 1); glEnd(); glTranslatef((float)-x*2,(float)-y*2,(float)-z*2); }
The Visual Studio DOMWalkerWizard
Now that you have a clear idea of how DOM visitors are implemented with MSXML3, you can factorize the programming portions common to all visitors. Therefore, you can reuse them and concentrate on the DOM visitor logic, instead of writing the same XML-handling code over and over.
The previous Framework, XMLableFR (which is part of the original chapter), was implemented with an open-source Unix system in mind (even though it is runnable and generates correct code for Windows, too). It is based on a free library, it has a command-line interface, it is written in Perl, and it makes no assumptions about your development environment. The next Framework, DOMWalkerWizard, is the other side of the coin: it assumes you are using a commercial product, MSXML3, under Windows, and you are programming using Visual Studio. Both poles are valid, and chances are you have to develop in both of them. I hope you find the broad mix of technologies useful.
What to Factorize?
By inspecting the BlockWorld example, you can see the following common programming elements in all Walkers:
Decide how to traverse the tree (for example, Preorder, Postorder, or Inorder)
Decide what types of nodes to take into consideration (for example, only treat elements)
Sometimes create a helper class to read the DOM tree from an external source (generally a file)
Our task is to implement this common behavior in the most reusable way, congruent with MSXML and the Visual Studio mechanisms.