具有设计模式风格的 XML 解析器和编辑器






4.86/5 (14投票s)
一个非常通用的 XML 解析器,其内部实现可以更改,而不会影响其余源代码。
引言
XML 已成为一种非常流行的语言,用于数据表示/GUI 设计/服务器之间通信等,但用于解析此语言的工具各不相同,从基于 COM 的(例如 MS-XML)、SAX 解析器到 Xerces 等等。大约每隔几周,就会出现一个新的解析工具,提供更好、更有效的解决方案,但不幸的是,你不能因为新的解析工具(抱歉,我指的是套件)问世了就到处修改解析代码。因此,我提出了(如果我敢这么称呼的话)桥接模式(Bridge Pattern)的一个变体,你将使用一个简单的解析器作为任何 XML 解析库的包装器,但如果解析器的内部实现发生变化,你无需更改其余源代码。
问题
实际上,乍一看,这似乎是一个微不足道的问题,因为一个精通设计模式的人可以轻松地为一个通用解析器(其内部更改不会反映在上层)提出解决方案。但深入研究,你会发现一个问题,那就是,XML 解析器(由任何第三方库提供)将包含大量类来表示文档、元素、节点和属性。现在,如果你想按照 GOF(四人帮设计模式)的方式构建自己的解析器,你必须为这些类中的每一个提供一个包装器,然后为所有这些类编写一些方法,并导出一个独立于被包装类的接口,但调用接口方法最终将调用必要的内部方法。
例如,如果 Xerces 解析器具有名为 XercesDOMParser
、XercesDOMDocument
和 XercesDOMNode
的数据结构,其中每个结构都有方法,如 XercesDOMParser
的 virtual DOMDocument* getDocument()
、DOMDocument
的 DOMElement *getDocumentElement()
,以及 DOMNode
的 virtual DOMNodeList *getChildNodes() const
,那么你必须为这些类提供相应的包装器,并映射到上述方法;这意味着,你必须创建一个另一个负担沉重的解析器,只是解析部分将由内部(被包装的)解析器完成。然后,这里有一个小问题,如何用最少的努力来对齐你自己定义的类,以实际表示 XML 解析树。
我给出的例子实际上是假设我将使用某种组合的抽象工厂模式和桥接模式来表示 XML 数据结构。也许有人可以指出一种我目前想不出的更好、更易于使用的模式来构建一个简单的通用 XML 解析器。
解决方案
解决方案在于保持一切都非常简单。与其为 XercesDOMParser
、DOMDocument
、DOMNode
等单个类编写包装器,不如创建一些有用的数据结构,例如 TXMLNode
来表示节点,TXMLAttrib
来表示属性,TXMLRoot
来表示根元素,以及最后但同样重要的是 TXMLParser
,一个完全抽象的类(只有纯虚方法),来表示解析器。解析器将包含大部分用于设置/获取节点、设置/获取底层属性/值等的方法。
遵循这个想法,DOMDocument
的 virtual DOMElement *getDocumentElement()
将映射到 TXMLParser
本身的 errno_t GetRoot(/*OUT*/TXMLRoot& root)
这样的方法,其中 TXMLRoot
表示文档元素。
我们都知道,所有 XML 文件都必须有一个根项,通常称为文档元素。在解析任何 XML 文件时,我们首先查找文档元素或根,它通过解析器的 virtual errno_t GetRoot(/*OUT*/TXMLRoot& root)
暴露。另一方面,内部文档的创建以及文档本身将完全隐藏在解析器内部。文档的创建是通过调用我们简单的 XML 解析器的以下方法来完成的:
// creates a new document
errno_t CreateNewDocument();
// loads and parse the XML file
errno_t OpenDocument(const TCHAR* path)
我们简单的 XML 解析器。
同样,其他操作与我给出的第一个 GetRoot(...)
的例子类似。
现在,让我们来详细看看表示 XML 树所需的简单数据结构
class TXMLNode
//In our simplified view, an element and a node more or less means the same
{
public:
enum NodeType
{
UNKNOWN = 0,
ELEMENT_NODE = 1,
ATTRIBUTE_NODE = 2,
TEXT_NODE = 3,
CDATA_SECTION_NODE = 4,
ENTITY_REFERENCE_NODE = 5,
ENTITY_NODE = 6,
PROCESSING_INSTRUCTION_NODE = 7,
COMMENT_NODE = 8,
DOCUMENT_NODE = 9,
DOCUMENT_TYPE_NODE = 10,
DOCUMENT_FRAGMENT_NODE = 11,
NOTATION_NODE = 12,
NODE_TYPE_COUNT = 13
};
public:
TXMLNode()
{
_nodeName = NULL;
_nodeValueString = NULL;
_nodeType = 0;
_internalNode = NULL;
}
~TXMLNode()
{
if(_nodeName != NULL)
{
delete[] _nodeName;
}
if(_nodeValueString != NULL)
{
delete[] _nodeValueString;
}
}
const TCHAR* GetName()
{
return _nodeName;
}
void SetName(const TCHAR* name)
{
CopyString(_nodeName, name);
}
const TCHAR* GetValueString()
{
return _nodeValueString;
}
void SetValueString(const TCHAR* val)
{
CopyString(_nodeValueString, val);
}
unsigned short GetNodeType()
{
return _nodeType;
}
const TCHAR* GetTextContent();
void* GetInternalNode()
{
return _internalNode;
}
void SetInternalNode(void* internalNode)
{
_internalNode = internalNode;
}
void SetNodeType(unsigned short type)
{
_nodeType = type;
}
void CopyString(TCHAR*& des, const TCHAR* src)
{
if(des != NULL )
{
delete[] des;
des = NULL;
}
if(src != NULL)
{
size_t len = _tcslen(src);
des = new TCHAR[len + 1];
memcpy(des, src, sizeof(TCHAR) * len);
des[len] = 0;
}
}
TCHAR* _nodeName;
TCHAR* _nodeValueString;
unsigned short _nodeType;
void* _internalNode;
};
class TXMLRoot : public TXMLNode
{
public:
TXMLRoot()
{
TXMLNode();
}
};
class TXMLAttrib : public TXMLNode
{
};
如你所见,这些类实际上并没有直接包装 Xerces 解析器的 DOMNode
或 DOMElement
。相反,所有这些类都包含一个 void 指针来保存相应的内部数据(例如,TXMLNode
内部有一个 DOMNode
指针,作为 void 指针)。TXMLParser
派生类的责任是实际理解 void 指针数据,并根据内部实现填充 TXMLNode
、TXMLRoot
或 TXMLAttrib
信息。以下是一个派生类如何使用内部数据的示例:
bool TXMLParserXerces::GetParentNode(TXMLNode* childNode, TXMLNode& node)
{
bool bRet = false;
INTERNAL_NODE* internalNode = (INTERNAL_NODE*)childNode->GetInternalNode();
if(internalNode != NULL)
{
INTERNAL_NODE* xmlNode = internalNode->getParentNode();
if(xmlNode != NULL)
{
PopulateNodeData(xmlNode, &node);
bRet = true;
}
}
return bRet;
}
你可以从上面的例子中猜到,TXMLParserXerces
是一个 TXMLParser
派生类。
TXMLParser
(抽象)类具有以下结构,并且方法名称足够明确,可以理解其功能,从而可以在具体类中轻松实现它们:
class TXMLParser
{
public:
enum ParserError
{
NO_XML_ERROR = 0,
INVALID_XML_DOCUMENT,
INVALID_ROOT_ELEMENT,
DETACHED_XML_NODE,
INVALID_XML_ELEMENT,
INVALID_NODE_NAME,
CREATE_IMPL_FAILED,
CREATE_DOC_FAILED,
SAVETO_FILE_FAILED,
CREATE_ELEMENT_FAILED,
SET_ATTRIB_FAILED,
REMOVE_CHILD_FAILED,
INSERT_BEFORE_FAILED,
REPLACE_CHILD_FAILED,
REMOVE_ATTRIB_FAILED,
UPDATE_DATA_FAILED
};
TXMLParser(bool simplified = true)
{
}
virtual ~TXMLParser(void)
{
}
virtual void Release() = 0;
virtual errno_t CreateNewDocument() = 0;
//loads and parse the XML file
virtual errno_t OpenDocument(const TCHAR* path) = 0;
virtual errno_t SaveDocument(const TCHAR* path) = 0;
virtual errno_t SaveToStream(TCHAR*& buf, unsigned int& len) = 0;
virtual errno_t CloseDocument() = 0;
virtual errno_t GetRoot(/*OUT*/TXMLRoot& root) = 0;
virtual unsigned int GetChildCount(TXMLNode* parentNode) = 0;
virtual bool GetChild(unsigned int index,
TXMLNode* parentNode, /*OUT*/TXMLNode& node) = 0;
// to use in conjunction with GetPrevSibling and GetNextSibling
virtual bool GetFirstChild(TXMLNode* parentNode, /*OUT*/TXMLNode& node) = 0;
// to use in conjunction with GetPrevSibling and GetNextSibling
virtual bool GetLastChild(TXMLNode* parentNode, /*OUT*/TXMLNode& node) = 0;
virtual bool GetNextSibling(TXMLNode* curNode, /*OUT*/TXMLNode& node) = 0;
virtual bool GetPrevSibling(TXMLNode* curNode, /*OUT*/TXMLNode& node) = 0;
virtual bool GetParentNode(TXMLNode* childNode, /*OUT*/TXMLNode& node) = 0;
virtual unsigned int GetAttribCount(TXMLNode* node) = 0;
virtual bool GetAttrib(unsigned int index,
TXMLNode* node, /*OUT*/TXMLAttrib& attrib) = 0;
virtual bool FindNode(const TCHAR* nodeName, /*OUT*/TXMLNode& node) = 0;
virtual bool FindNode(const TCHAR* nodeName,
TXMLNode* curNode, /*OUT*/TXMLNode& node) = 0;
//The following two functions are very unlikely
//to be used, put in there just incase
virtual bool FindNodeByNameVal(const TCHAR* nodeName,
const TCHAR* nodeVal, /*OUT*/TXMLNode& node) = 0;
virtual bool FindNodeByNameVal(const TCHAR* nodeName,
const TCHAR* nodeVal, TXMLNode* curNode, /*OUT*/TXMLNode& node) = 0;
virtual bool FindAttrib(const TCHAR* nodeName,
const TCHAR* attribName, TXMLAttrib& attrib) = 0;
virtual bool FindAttrib(TXMLNode* node,
const TCHAR* attribName, /*OUT*/TXMLAttrib& attrib) = 0;
virtual bool IsLoaded() = 0;
// Do not delete the returned node externally, parser will take care of it
virtual TXMLNode* CreateNodeInstance() = 0;
// Do not delete the returned node externally, parser will take care of it
virtual TXMLRoot* CreateRootInstance() = 0;
// Do not delete the returned node externally, parser will take care of it
virtual TXMLAttrib* CreateAttribInstance() = 0;
virtual errno_t AddRoot(TXMLRoot* rootNode) = 0;
virtual errno_t RemoveRoot() = 0;
virtual errno_t AddChild(TXMLNode* parentNode, TXMLNode* childNode) = 0;
virtual errno_t RemoveChild(TXMLNode* parentNode,
/*INOUT*/TXMLNode*& childNode) = 0;
virtual errno_t InsertBefore(TXMLNode* parentNode,
TXMLNode* newNode, TXMLNode* refNode) = 0;
virtual errno_t ReplaceChild(TXMLNode* parentNode,
TXMLNode* newNode, TXMLNode* oldNode) = 0;
virtual errno_t ReplaceNodeName(TXMLNode* node, const TCHAR* name) = 0;
virtual errno_t ReplaceNodeValue(TXMLNode* node, const TCHAR* value) = 0;
//if attrib already exists then sets the attrib otherwise adds the attrib
virtual errno_t SetAttrib(TXMLNode* node, TXMLAttrib* attrib) = 0;
virtual errno_t RemoveAttrib(TXMLNode* node, const TCHAR* attribName) = 0;
protected:
virtual const TCHAR* GetTextContent(TXMLNode* xmlNode) = 0;
virtual bool UpdateUnderlyingData(TXMLNode* xmlNode) = 0;
};
在整个代码中,你只会使用 TXMLParser
类数据类型的引用(或指针),并定义一个实现类,如 TXMLParserXerces
,来执行内部处理和 XML 数据检索。
Using the Code
以下是使用我们简单的解析器打开 XML 格式文档并保存它的快速概览:
// CTXMLEditorDoc commands
BOOL CTXMLEditorDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
if (!CDocument::OnOpenDocument(lpszPathName))
return FALSE;
if(GetXMLParser() == NULL)
return FALSE;
// TODO: Add your specialized creation code here
bool bRet = (GetXMLParser()->OpenDocument(lpszPathName) ==
TXMLParser::NO_XML_ERROR) ? true : false;
if(!bRet)
AfxMessageBox(L"Sorry, unable to open file");
return bRet ? TRUE : FALSE;
}
BOOL CTXMLEditorDoc::OnSaveDocument(LPCTSTR lpszPathName)
{
if(GetXMLParser() == NULL)
return FALSE;
bool bRet = (GetXMLParser()->SaveDocument(lpszPathName) ==
TXMLParser::NO_XML_ERROR) ? true : false;
if(!bRet)
{
AfxMessageBox(L"Sorry, unable to save file");
}
else
{
CDocument::SetModifiedFlag(FALSE);
}
return bRet ? TRUE : FALSE;
}
以下是将 XML 数据填充到树控件中的代码片段:
void CTXMLEditorView::OnUpdate(CView* /*pSender*/, LPARAM /*lHint*/, CObject* /*pHint*/)
{
// TODO: Add your specialized code here and/or call the base class
GetTreeCtrl().SetBkColor(RGB(255, 255, 255));
GetTreeCtrl().SetTextColor(RGB(80, 60, 240));
GetTreeCtrl().SetLineColor(RGB(200, 100, 100));
GetTreeCtrl().SetItemHeight(16);
CTXMLEditorDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if (!pDoc)
return;
TXMLParser* pParser = pDoc->GetXMLParser();
if(pParser && pParser->IsLoaded())
{
//Populate Tree Ctrl from the root
TXMLRoot* root = pParser->CreateRootInstance();
if(pParser->GetRoot(*root) == TXMLParser::NO_XML_ERROR)
{
PopulateTreeCtrl(*pParser, root, NULL);
}
}
}
void CTXMLEditorView::PopulateTreeCtrl(TXMLParser& parser,
TXMLNode* curNode, HTREEITEM hParent)
{
TXMLNode* orgNode = curNode;
CString nodeName = orgNode->GetName();
CString nodeValue = orgNode->GetValueString();
TCHAR buf[2] = {0, 0};
if(nodeValue.GetLength() > 1)
{
buf[0] = nodeValue.GetAt(0);
buf[1] = nodeValue.GetAt(1);
}
CString nodeNameVal = nodeName;
if(nodeValue.GetLength() && buf[0] != '\n' && buf[1] != '\r')
{
nodeNameVal += NAME_VALUE_SEPARATOR;
nodeNameVal += nodeValue;
}
TVINSERTSTRUCT tvInsert;
tvInsert.hParent = hParent;
tvInsert.hInsertAfter = TVI_LAST;
tvInsert.item.mask = TVIF_TEXT;
tvInsert.item.pszText = (TCHAR*)(LPCTSTR)nodeNameVal;
tvInsert.item.lParam = NULL;
HTREEITEM hItem = GetTreeCtrl().InsertItem(&tvInsert);
tvInsert.hParent = hItem;
tvInsert.hInsertAfter = TVI_LAST;
tvInsert.item.mask = TVIF_TEXT;
tvInsert.item.pszText = ATTRIBUTES_TAG;
HTREEITEM hAttrib = GetTreeCtrl().InsertItem(&tvInsert);
unsigned int attribCount = parser.GetAttribCount(orgNode);
if(attribCount)
{
TXMLAttrib* attrib = parser.CreateAttribInstance();
for(unsigned int i = 0; i < attribCount; i++)
{
if(parser.GetAttrib(i, orgNode, *attrib))
{
tvInsert.hParent = hAttrib;
CString attribNameVal;
attribNameVal = attrib->GetName();
attribNameVal += NAME_VALUE_SEPARATOR;
attribNameVal += attrib->GetValueString();
tvInsert.item.pszText = (TCHAR*)(LPCTSTR)attribNameVal;
HTREEITEM hAttribNameVal = GetTreeCtrl().InsertItem(&tvInsert);
}
}
}
tvInsert.hParent = hItem;
tvInsert.hInsertAfter = TVI_LAST;
tvInsert.item.mask = TVIF_TEXT;
tvInsert.item.pszText = CHILD_NODES_TAG;
HTREEITEM hChildren = GetTreeCtrl().InsertItem(&tvInsert);
unsigned int childCount = parser.GetChildCount(curNode);
if(childCount)
{
TXMLNode* nextNode = parser.CreateNodeInstance();
bool bRet = parser.GetFirstChild(curNode, *nextNode);
while(bRet)
{
PopulateTreeCtrl(parser, nextNode, hChildren);
curNode = nextNode;
bRet = parser.GetNextSibling(curNode, *nextNode);
}
}
}
TXMLParser
的纯虚方法的完整实现可以在 TXMLParserXerces
类中找到,该类可在上传的源代码中找到。
关注点
在实现 XML 文件读取的同时,我还添加了 XML 文件保存机制,所以最终,该解析器与其说是一个 XML 编辑器,不如说是一个 XML 编辑器。在上传的源代码中,使用树控件来表示 XML 节点,每个树节点有两个默认的子项:Attribute(属性)和 Child Nodes(子节点)。在 Attribute 子项下,节点的属性以 AttributeName=Value 的格式列出,在 Child Nodes 子项下,子 XML 节点就像父节点一样表示。如果一个节点有值,则节点在树项中以 nodename=value 的格式表示。
致谢
Jacques Raphanel:感谢他将我介绍给 Xerces Parser 库(该库在 Windows 以外的操作系统上也易于移植),我发现它比基于 COM 的 MS-XML 库更容易实现。
John Adams:感谢他帮助我纠正了将 Adapter 误称为 Bridge 的错误。在这篇文章中,凡是看到“Bridge Pattern”字样的地方,之前都被错误地称为“Adapter Pattern”。
历史
- 文章上传日期:2010 年 8 月 16 日。