/* * Portable Agile C++ Classes (PACC) * Copyright (C) 2001-2003 by Marc Parizeau * http://manitou.gel.ulaval.ca/~parizeau/PACC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Contact: * Laboratoire de Vision et Systemes Numeriques * Departement de genie electrique et de genie informatique * Universite Laval, Quebec, Canada, G1K 7P4 * http://vision.gel.ulaval.ca * */ /*! * \file PACC/XML/Node.cpp * \brief Class methods for the %XML parse tree node. * \author Marc Parizeau, Laboratoire de vision et systèmes numériques, Université Laval * $Revision: 1.7.2.2 $ * $Date: 2007/09/10 18:24:10 $ */ #include "XML/Node.hpp" #include "XML/Iterator.hpp" #include "XML/Streamer.hpp" #include "Util/Assert.hpp" #include #include using namespace std; using namespace PACC; map XML::Node::smMap; /*! */ XML::Node::Node(void) : mType(eRoot) { mParent = mFirstChild = mLastChild = mPrevSibling = mNextSibling = NULL; } /*! */ XML::Node::Node(const string& inValue, XML::NodeType inType) : mType(inType) { (*this)[""] = inValue; mParent = mFirstChild = mLastChild = mPrevSibling = mNextSibling = NULL; } /*! */ XML::Node::Node(const string& inValue, const XML::AttributeList& inAttrList) : AttributeList(inAttrList), mType(eData) { (*this)[""] = inValue; mParent = mFirstChild = mLastChild = mPrevSibling = mNextSibling = NULL; } /*! */ XML::Node::Node(const XML::Node& inNode) : AttributeList() { mParent = mFirstChild = mLastChild = mPrevSibling = mNextSibling = NULL; operator=(inNode); } /*! This method recursively deletes all of its children. */ XML::Node::~Node(void) { // delete all child nodes eraseChildren(); // detach from parent and siblings detachFromSiblingsAndParent(); // cleanup node pointers mParent = mFirstChild = mLastChild = mPrevSibling = mNextSibling = NULL; } /*! \return A reference to this node \attention The copied tree must not be a sub-tree of this node. Otherwise, the internal tree structure will become corrupted. */ XML::Node& XML::Node::operator=(const Node& inRoot) { // do not self assign! if(&inRoot == this) return *this; // delete all child nodes XML::Iterator lChild = getFirstChild(); while(lChild) delete &(*(lChild++)); // fix child pointers mFirstChild = mLastChild = NULL; // assign type and attributes mType = inRoot.mType; map::operator=(inRoot); // copy all children of inRoot for(XML::ConstIterator lNode = inRoot.getFirstChild(); lNode; ++lNode) { // allocate and copy node Node* lChildNode = new Node(*lNode); // is this the first child? if(mFirstChild == NULL) mFirstChild = mLastChild = lChildNode; else { //adjust sibling pointers mLastChild->mNextSibling = lChildNode; lChildNode->mPrevSibling = mLastChild; mLastChild = lChildNode; } // adjust parent pointer lChildNode->mParent = this; } return *this; } /*! \return A reference to the converted string. The default quotes are "&", "<", ">", "'", and """. Argument \c ioMap can be used to specify any conversion table. */ string& XML::Node::convertFromQuotes(string& ioString, map& ioMap) { if(ioMap.empty()) { // initialize quote list ioMap["amp"] = '&'; ioMap["lt"] = '<'; ioMap["gt"] = '>'; ioMap["apos"] = '\''; ioMap["quot"] = '"'; } string::size_type lStart, lEnd = 0; while((lStart = ioString.find('&', lEnd)) < ioString.size() && (lEnd = ioString.find(';', lStart)) < ioString.size()) { string lToken = ioString.substr(lStart+1, lEnd-lStart-1); if(ioMap.find(lToken) != ioMap.end()) { ioString[lStart] = ioMap[lToken]; ioString.erase(lStart+1, lEnd-lStart); lEnd = lStart+1; } } return ioString; } /*!\return A pointer to this node. This method removes this node from its parent tree. The list of sibling nodes is repaired accordingly. */ XML::Node* XML::Node::detachFromSiblingsAndParent(void) { // adjust sibling list if(mPrevSibling) mPrevSibling->mNextSibling = mNextSibling; if(mNextSibling) mNextSibling->mPrevSibling = mPrevSibling; if(mParent) { // adjust parent first and last child pointers if(mParent->mFirstChild == this) mParent->mFirstChild = mNextSibling; if(mParent->mLastChild == this) mParent->mLastChild = mPrevSibling; } mPrevSibling = mNextSibling = mParent = NULL; return this; } /*! */ void XML::Node::eraseChildren(void) { // delete all child nodes XML::Iterator lChild = getFirstChild(); while(lChild) delete &(*(lChild++)); } /*!\return The number of child nodes. */ unsigned int XML::Node::getChildCount(void) const { unsigned int lCount = 0; for(ConstIterator lChild = getFirstChild(); lChild; ++lChild) ++lCount; return lCount; } /*!\return A pointer to the inserted child node. */ XML::Node* XML::Node::insertAsLastChild(XML::Node* inChild) { PACC_AssertM(inChild, "Cannot add null pointer node"); PACC_AssertM(!inChild->mParent && !inChild->mPrevSibling && !inChild->mNextSibling, "Node must be detached before it can be added!"); // is this new child the first? if(mFirstChild == NULL) mFirstChild = inChild; else { // insert after last inChild->mPrevSibling = mLastChild; mLastChild->mNextSibling = inChild; } // adjust parent pointers inChild->mParent = this; mLastChild = inChild; return inChild; } /*!\return A pointer to the inserted sibling node. */ XML::Node* XML::Node::insertAsPreviousSibling(XML::Node* inSibling) { PACC_AssertM(inSibling, "Cannot insert null pointer node"); PACC_AssertM(!inSibling->mParent && !inSibling->mPrevSibling && !inSibling->mNextSibling, "Node must be detached before it can be inserted!"); // is this new sibling the first? if(mPrevSibling == NULL) { inSibling->mNextSibling = this; mPrevSibling = inSibling; // adjust first child of parent if(mParent) mParent->mFirstChild = inSibling; } else { // this node is neither the first or the last mPrevSibling->mNextSibling = inSibling; inSibling->mPrevSibling = mPrevSibling; inSibling->mNextSibling = this; mPrevSibling = inSibling; } // adjust parent pointer inSibling->mParent = mParent; return inSibling; } /*!\return A node pointer to the parsed element. Any tag name defined in \c inNoParseTags will be treated as if its content is a string token (content will not be parsed). */ XML::Node* XML::Node::parse(PACC::Tokenizer& inTokenizer, const set& inNoParseTags) { Node* lNode = NULL; // look for start tag string lToken; inTokenizer.setDelimiters("", "<"); if(!inTokenizer.getNextToken(lToken)) return 0; // remove any leading white space size_type lPos = lToken.find_first_not_of(" \t\r\n"); if(lPos == string::npos) { if(!inTokenizer.getNextToken(lToken)) return 0; } else if(lPos > 0) lToken.erase(0, lPos); if(lToken[0] == '<') { // check for end tag if(inTokenizer.peekNextChar() == '/') { // found end tag; inTokenizer.setDelimiters("", "/"); inTokenizer.getNextToken(lToken); return 0; } // found start tag lNode = new Node; lNode->parseStartTag(inTokenizer, lToken); if(lToken[0] == '/') { // found end tag; next token must be '>' inTokenizer.setDelimiters("", ">"); if(!inTokenizer.getNextToken(lToken)) lNode->throwError(inTokenizer, "unexpected eof"); if(lToken[0] != '>') lNode->throwError(inTokenizer, "invalid start tag"); } else if(lNode->getType() == eData) { // either read or parse tag content if(inNoParseTags.find((*lNode)[""]) != inNoParseTags.end()) { lNode->readContentAsString(inTokenizer); } else { Node* lChild; // parse all child while((lChild=parse(inTokenizer, inNoParseTags)) != NULL) lNode->insertAsLastChild(lChild); // test for valid end tag inTokenizer.setDelimiters("", " \t\n\r>"); if(!inTokenizer.getNextToken(lToken)) lNode->throwError(inTokenizer, "unexpected eof"); if(lToken != (*lNode)[""]) lNode->throwError(inTokenizer, "invalid end tag"); } // next token must be '>' inTokenizer.setDelimiters(" \t\n\r", ">"); if(!inTokenizer.getNextToken(lToken)) lNode->throwError(inTokenizer, "unexpected eof"); if(lToken[0] != '>') lNode->throwError(inTokenizer, "invalid end tag"); } // else node is not markup } else { // found a simple string node lNode = new Node; lNode->mType = eString; // remove any ending white space lPos = lToken.find_last_not_of(" \t\r\n"); PACC_AssertM(lPos != string::npos, "Internal error!"); if(lPos < lToken.size()-1) lToken.resize(lPos+1); // convert basic quotes (*lNode)[""] = convertFromQuotes(lToken); } return lNode; } /*! Ending token is returned through argument \c outToken. */ void XML::Node::parseAttributeList(PACC::Tokenizer& inTokenizer, string& outToken) { inTokenizer.setDelimiters(" \t\n\r", "=/?>"); // next token should be an attribute name if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); // parse all attributes while(outToken[0] != '>' && outToken[0] != '/' && outToken[0] != '?') { if(outToken[0] == '=') throwError(inTokenizer, "missing attribute name"); // ok, found an attribute name! string lName = outToken; // next token should be '=' inTokenizer.setDelimiters(" \t\n\r", "="); if(!inTokenizer.getNextToken(outToken) || outToken[0] != '=') throwError(inTokenizer, "invalid attribute"); inTokenizer.setDelimiters(" \t\n\r", "'\""); // next token must be '"' or "'" if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); string lValue; switch(outToken[0]) { case '\'': inTokenizer.setDelimiters("", "'"); if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); if(outToken[0] != '\'') { lValue = outToken; if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); } break; case '"': inTokenizer.setDelimiters("", "\""); if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); if(outToken[0] != '"') { lValue = outToken; if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); } break; default: throwError(inTokenizer, "invalid attribute value"); } // insert attribute (*this)[lName] = convertFromQuotes(lValue); inTokenizer.setDelimiters(" \t\n\r", "=/?>"); if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); } } /*! This method assumes that token "<" has already been read. It returns the ending token through argument \c outToken. */ void XML::Node::parseStartTag(PACC::Tokenizer& inTokenizer, string& outToken) { // parse tag name inTokenizer.setDelimiters("", " \t\n\r/>"); if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); if(outToken.find_first_of(" \t\n\r/>") != string::npos) throwError(inTokenizer, "invalid start tag"); string& lValue = (*this)[""]; switch(outToken[0]) { case '!': if(outToken.size() >= 3 && outToken[1] == '-' && outToken[2] == '-') { // process comment mType = eComment; outToken.erase(0, 3); inTokenizer.setDelimiters("", ">"); do { int lSize = outToken.size(); if(lSize > 2 && outToken[lSize-2] == '-' && outToken[lSize-1] == '-') { lValue += outToken.erase(lSize-2, 2); break; } else lValue += outToken; } while(inTokenizer.getNextToken(outToken)); if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); } else if(outToken.size() >= 8 && memcmp(outToken.data()+1, "[CDATA[", 7) == 0) { // process cdata section mType = eCDATA; outToken.erase(0, 8); inTokenizer.setDelimiters("", ">"); do { int lSize = outToken.size(); if(lSize >= 2 && outToken[lSize-2] == ']' && outToken[lSize-1] == ']') { lValue += outToken.erase(lSize-2, 2); break; } else lValue += outToken; } while(inTokenizer.getNextToken(outToken)); if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); } else { // process special element (doctype, attribute, etc.) mType = eSpecial; lValue = outToken.erase(0, 1); inTokenizer.setDelimiters("", ">"); if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); if(outToken[0] != '>') { lValue += outToken; if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); } } break; case '?': if(outToken == "?xml") { // process xml declaration mType = eDecl; lValue = outToken.erase(0, 1); parseAttributeList(inTokenizer, outToken); if(outToken[0] != '?') throwError(inTokenizer, "invalid xml declaration"); if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); if(outToken[0] != '>') throwError(inTokenizer, "invalid xml declaration"); } else { // process special processing instruction mType = ePI; lValue = outToken.substr(1, outToken.size()-1); inTokenizer.setDelimiters("", "?>"); while(inTokenizer.getNextToken(outToken)) { if(outToken[0] == '?') { if(!inTokenizer.getNextToken(outToken)) throwError(inTokenizer, "unexpected eof"); if(outToken[0] == '>') break; else lValue += '?'; } lValue += outToken; } if(outToken.empty()) throwError(inTokenizer, "unexpected eof"); } break; default: // process data markup mType = eData; lValue = outToken; parseAttributeList(inTokenizer, outToken); } } /*! */ void XML::Node::readContentAsString(PACC::Tokenizer& inTokenizer) { // create child node Node* lChild = new Node; insertAsLastChild(lChild); lChild->setType(eNoParse); // parse until end tag inTokenizer.setDelimiters("", "<>"); string lToken; int lCount = 1; const string& lTag = (*this)[""]; string& lString = (*lChild)[""]; while(lCount > 0) { // check every start tag if(!inTokenizer.getNextToken(lToken)) throwError(inTokenizer, string("unexpected eof")); if(lToken[0] == '<') { if(!inTokenizer.getNextToken(lToken)) throwError(inTokenizer, string("unexpected eof")); if(lToken[0] == '/' && memcmp(lToken.data()+1, lTag.data(), lTag.size()) == 0) --lCount; else if(lToken[lToken.size()-1] != '/' && memcmp(lToken.data(), lTag.data(), lTag.size()) == 0) ++lCount; if(lCount > 0) { lString += "<"; lString += lToken; } } else lString += lToken; } // remove any leading white space size_type lPos = lString.find_first_not_of(" \t\r\n"); if(lPos == string::npos) { // string is all white space lString.clear(); } else { // erase leading white space lString.erase(0, lPos); // remove any ending white space lPos = lString.find_last_not_of(" \t\r\n"); PACC_AssertM(lPos != string::npos, "Internal error!"); // erase trailing white space if(lPos < lString.size()-1) lString.resize(lPos+1); } } /*! Argument \c inIndent is used to control indentation. By default (\c inIndent=true), the sub-tree rooted by this node will be serialized with indentation. If \c inIndent=false, then the node will be serialized without any form of indentation (including line feeds). */ void XML::Node::serialize(XML::Streamer& outStream, bool inIndent) const { switch(mType) { case eCDATA: { outStream.insertCDATA(getValue()); break; } case eComment: { outStream.insertComment(getValue()); break; } case eData: { // check for tag with single string content ConstIterator lChild = getFirstChild(); if(lChild && lChild->mType == eString && !lChild->getNextSibling()) { // disable indentation outStream.openTag(getValue(), false); } else { outStream.openTag(getValue(), inIndent); } // serialize attribute list for(map::const_iterator i = begin(); i != end(); ++i) { if(i->first != "") outStream.insertAttribute(i->first, i->second); } // serialize child nodes while(lChild) (lChild++)->serialize(outStream, inIndent); outStream.closeTag(); break; } case eNoParse: { outStream.insertStringContent(getValue(), false); break; } case ePI: { string lValue = string(""); outStream.insertStringContent(lValue, false); break; } case eSpecial: { string lValue = string(""); outStream.insertStringContent(lValue, false); break; } case eString: { outStream.insertStringContent(getValue(), true); break; } case eDecl: { string lValue = ""; outStream.insertStringContent(lValue, false); break; } default: { PACC_AssertM(false, "Unknown node type!"); } } } /*! */ void XML::Node::throwError(PACC::Tokenizer& inTokenizer, const string& inMessage) const { ostringstream lStream; lStream << "\nXML parse error"; if(inTokenizer.getStreamName() != "") lStream << " in file \"" << inTokenizer.getStreamName() << "\","; lStream << " at line "; lStream << inTokenizer.getLineNumber(); switch(mType) { case eCDATA: lStream << "\nfor CDATA \""; break; case eComment: lStream << "\nfor comment \""; break; case eData: lStream << "\nfor markup \""; break; case ePI: lStream << "\nfor processing instruction \""; break; case eRoot: lStream << "\nfor root element \""; break; case eSpecial: lStream << "\nfor special element \""; break; case eString: lStream << "\nfor literal string \""; break; case eDecl: lStream << "\nfor declaration \""; break; default: lStream << "\nfor unknown element \""; break; } if(getValue().size() < 40) lStream << getValue() << "\": " << inMessage; else lStream << getValue().substr(0,40) << "...\": " << inMessage; throw runtime_error(lStream.str()); } /*! */ ostream& PACC::operator<<(ostream &outStream, const XML::Node& inNode) { XML::Streamer lStream(outStream); inNode.serialize(lStream); return outStream; } /*! */ istream& PACC::operator>>(istream &inStream, XML::Node& outNode) { Tokenizer lTokenizer(inStream); XML::Node* lNode = XML::Node::parse(lTokenizer, set()); outNode = *lNode; delete lNode; return inStream; }