How do I run XPath queries in QT? - c++

How do I run an XPath query in QT?
I need to sort out certain tags with specific values in a certain attribute. The QXmlQuery documentation is anything but legible.
The schema I'm parsing is the Rhythmbox DB format:
<rhythmdb version="1.6">
<entry type="ignore">
<title></title>
<genre></genre>
<artist></artist>
<album></album>
<location>file:///mnt/disk/music/Cover.jpg</location>
<mountpoint>file:///mnt/disk</mountpoint>
<mtime>1222396828</mtime>
<date>0</date>
<mimetype>application/octet-stream</mimetype>
<mb-trackid></mb-trackid>
<mb-artistid></mb-artistid>
<mb-albumid></mb-albumid>
<mb-albumartistid></mb-albumartistid>
<mb-artistsortname></mb-artistsortname>
</entry>
<entry type="song">
<title>Bar</title>
<genre>Foobared Music</genre>
<artist>Foo</artist>
<album>The Great big Bar</album>
<track-number>1</track-number>
<disc-number>1</disc-number>
<duration>208</duration>
<file-size>8694159</file-size>
<location>file:///media/disk/music/01-Foo_-_Bar.ogg
<mountpoint>file:///media/disk
<mtime>1216995840</mtime>
<first-seen>1250478814</first-seen>
<last-seen>1250478814</last-seen>
<bitrate>301</bitrate>
<date>732677</date>
<mimetype>application/x-id3</mimetype>
<mb-trackid></mb-trackid>
<mb-artistid></mb-artistid>
<mb-albumid></mb-albumid>
<mb-albumartistid></mb-albumartistid>
<mb-artistsortname></mb-artistsortname>
</entry>
</rhythmdb>
This is your basic XML Schema which has a collection of structured entries. My intention was to filter out the entries with the type 'ignore'.

The relevant documentation is at: http://qt-project.org/doc/qt-4.8/qxmlquery.html#running-xpath-expressions.
The solution I came to was to use QXmlQuery to generate an XML file then parse it again using QDomDocument.
RhythmboxTrackModel::RhythmboxTrackModel()
{
QXmlQuery query;
QXmlQuery entries;
QString res;
QDomDocument rhythmdb;
/*
* Try and open the Rhythmbox DB. An API call which tells us where
* the file is would be nice.
*/
QFile db(QDir::homePath() + "/.gnome2/rhythmbox/rhythmdb.xml");
if ( ! db.exists()) {
db.setFileName(QDir::homePath() + "/.local/share/rhythmbox/rhythmdb.xml");
if ( ! db.exists())
return;
}
if (!db.open(QIODevice::ReadOnly | QIODevice::Text))
return;
/*
* Use QXmlQuery to execute and XPath query. Check the version to
* make sure.
*/
query.setFocus(&db);
query.setQuery("rhythmdb[#version='1.6']/entry[#type='song']");
if ( ! query.isValid())
return;
query.evaluateTo(&res);
db.close();
/*
* Parse the result as an XML file. These shennanigans actually
* reduce the load time from a minute to a matter of seconds.
*/
rhythmdb.setContent("" + res + "");
m_entryNodes = rhythmdb.elementsByTagName("entry");
for (int i = 0; i < m_entryNodes.count(); i++) {
QDomNode n = m_entryNodes.at(i);
QString location = n.firstChildElement("location").text();
m_mTracksByLocation[location] = n;
}
qDebug() << rhythmdb.doctype().name();
qDebug() << "RhythmboxTrackModel: m_entryNodes size is" << m_entryNodes.size();
}
In case anyone is wondering, this is my code taken from a recent branch of the Mixxx project, specifically the features_looping branch.
The things I dislike about this solution are:
Parsing the XML twice
Concatenating the result with a starting and ending tag.

If it fits your parsing requirements, you can use the SAX-based reader instead of a DOM-based one. Using QXmlSimpleReader with a sub-classed QXmlDefaultHandler, you can get access to each element of your XPath query as well as its attributes as the document is scanned. I think this approach would be faster than a DOM-based one; you don't have to read anything twice and it's already built into Qt. There is an example here: http://www.digitalfanatics.org/projects/qt_tutorial/chapter09.html under "Reading Using SAX."

Related

How to convert xml node data into string in QT [duplicate]

This question already has an answer here:
Reading an XML file using QXmlStreamReader
(1 answer)
Closed 6 years ago.
I have xml file in my local machine. The xml file format is like:
<string>
<Data>
<Name>Sanket</Name>
<Number>0987654321</Number>
<Address>India</Address>
</Data>
<Data>
<Name>Rahul</Name>
<Number>0987654321</Number>
<Address>Maharashtra</Address>
</Data>
</string>
I want to convert this XML file data into String format. Like:
Sanket 0987654321 India
Rahul 0987654321 Maharashtra
What is the easiest way to convert this data in QT using c++.
I am new in that, so please can anyone suggest me some sample code for this?
Thank you in advance.
I tried following code, but that not work for me:
void parseFile()
{
QList<QList<QString> > dataSet;
QString lastError = "";
QFile inFile("test.xml");
if (inFile.open(QIODevice::ReadOnly))
{
QTextStream fread(&inFile);
long totalSize = inFile.size();
QString line;
while(!fread.atEnd())
{
line = fread.readLine();
QList<QString> record = line.split(QString::KeepEmptyParts);
dataSet.append(record);
}
qDebug()<<dataSet;
}else{
lastError = "Could not open "+test.xml+" for reading";
}
}
You could parse the xml elements firstly via QXmlStreamReader and then you can assemble the xml elements into the string how you want.
The problem of you Code is that you only process the text Lines without any xml-syntax processed by the xml class.
You should look at the QtXML classes for which Florent Uguet provided some links.
However I modified the example found here to do what you want (It does that exact thing for your exact input):
#include <QDomDocument>
#include <QFile>
#include <iostream>
#include <QDomNodeList>
int main()
{
QDomDocument doc("mydocument");
QFile file("test.xml");
if (!file.open(QIODevice::ReadOnly))
return 1;
if (!doc.setContent(&file)) {
file.close();
return 1;
}
file.close();
const auto stringTags = doc.elementsByTagName("string");
for(int stringsI = 0; stringsI < stringTags.size(); ++stringsI){
QDomNode stringTag = stringTags.at(stringsI);
for(QDomNode dataTag = stringTag.firstChildElement("Data"); !dataTag.isNull(); dataTag = dataTag.nextSiblingElement("Data")){
for(QDomNode innerTag = dataTag.firstChild(); !innerTag.isNull(); innerTag = innerTag.nextSibling()){
auto val = innerTag.toElement().text();
std::cout << val.toStdString() << " ";
}
std::cout << std::endl;
}
}
return 0;
}
I build it with QtCreator using qmake. For this you should know that you need to put QT += xml in your *.pro file.
Already asked (and with code) : Reading an XML file using QXmlStreamReader
Qt provides a set of classes for handling XML :
http://doc.qt.io/qt-5.7/qtxml-index.html
http://doc.qt.io/qt-5.7/qxmlstreamreader.html
http://doc.qt.io/qt-5.7/qxmlstreamwriter.html
Old C++ classes (not maintained)
http://doc.qt.io/qt-5/qtxml-module.html
Once you have parsed your file using these, you can usually read the individual nodes' inner text or attributes.

Xerces XPath causes seg fault when path doesn't exist

I can successfully use Xerces XPath feature to query for information from an XML with the following XML and C++ code.
XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<root>
<ApplicationSettings>
hello universe
</ApplicationSettings>
</root>
C++
int main()
{
XMLPlatformUtils::Initialize();
// create the DOM parser
XercesDOMParser *parser = new XercesDOMParser;
parser->setValidationScheme(XercesDOMParser::Val_Never);
parser->parse("fake_cmf.xml");
// get the DOM representation
DOMDocument *doc = parser->getDocument();
// get the root element
DOMElement* root = doc->getDocumentElement();
// evaluate the xpath
DOMXPathResult* result=doc->evaluate(
XMLString::transcode("/root/ApplicationSettings"), // <-- HERE IS THE XPATH
root,
NULL,
DOMXPathResult::ORDERED_NODE_SNAPSHOT_TYPE, //DOMXPathResult::ANY_UNORDERED_NODE_TYPE, //DOMXPathResult::STRING_TYPE,
NULL);
// look into the xpart evaluate result
result->snapshotItem(0);
std::cout<<TranscodeToStr(result->getNodeValue()->getFirstChild()->getNodeValue(),"ascii").str()<<std::endl;;
XMLPlatformUtils::Terminate();
return 0;
}
The problem is that sometimes my XML will only have certain fields. But if I remove the ApplicationSettings entry from the XML it will seg fault. How can I properly handle these optional fields? I know that trying to correct from seg faults is risky business.
The seg fault is occurring in this line
std::cout<<TranscodeToStr(result->getNodeValue()->getFirstChild()->getNodeValue(),"ascii").str()<<std::endl;
specifically in get getFirstChild() call because the result of getNodeValue() is NULL.
This is my quick and dirty solution. It's not really ideal but it works. I would prefer a more sophisticated evaluation and response.
if (result->getNodeValue() == NULL)
{
cout << "There is no result for the provided XPath " << endl;
}
else
{
cout<<TranscodeToStr(result->getNodeValue()->getFirstChild()->getNodeValue(),"ascii").str()<<endl;
}

Read XML node with RapidXML

I'm using RapidXML to parse XML files and read nodes content but I don't want to read values inside a node, I need to read the content of specific XML nodes "as XML" not as parsed values.
Example :
<node1>
<a_lot_of_xml>
< .... >
</a_lot_of_xml>
</node1>
I need to get the content of node1 as :
<a_lot_of_xml>
< .... >
</a_lot_of_xml>
What I tired :
I tried something but its not really good in my opinion, its about to put in node1, the path of an other xml file to read, I did like this :
<file1ToRead>MyFile.xml</file1ToRead>
And then my c++ code is the following :
ifstream file(FileToRead);
stringstream buffer; buffer << file.rdbuf();
But the problem is users will have a lot of XML files to maintain and I just want to use one xml file.
I think "a lot of XML files" is a better way, so you have a directory of all xml files, you can read the xml file when you need it, good for performance.
Back to the problem, can use the rapidxml::print function to get the xml format.
bool test_analyze_xml(const std::string& xml_path)
{
try
{
rapidxml::file<> f_doc(xml_path.c_str());
rapidxml::xml_document<> xml_doc;
xml_doc.parse<0>(const_cast<char*>(f_doc.data()));
rapidxml::xml_node<>* node_1 = xml_doc.first_node("node1");
if(node_1 == NULL)
{
return false;
}
rapidxml::xml_node<>* plain_txt = node_1->first_node("a_lot_of_xml");
if (plain_txt == NULL)
{
return false;
}
std::string xml_data;
rapidxml::print(std::back_inserter(xml_data), *plain_txt, rapidxml::print_no_indenting); //the xml_data is XML format.
}
catch (...)
{
return false;
}
return true;
}
I'm unfamiliar with rapidxml, but I have done this with tinyxml2. The trick is to read out node1 and then create a new XMLDoc (using tinyxml2 terms here) that contains everything inside of node1. From there, you can use their XMLPrinter class to convert your new XMLDoc (containing everything in node1) to a string.
tinyxml2 is a free download.

Parsing XML Elements using TinyXML

UPDATE: Still not working :( I have updated the code portion to reflect what I currently have.
This should be a pretty easy question for people who have used TinyXML. I'm attempting to use TinyXML to parse through an XML document and pull out some values. I figured out how to add in the library yesterday, and I have successfully loaded the document (hey, it's a start).
I've been reading through the manual and I can't quite figure out how to pull out individual attributes. After Googling around, I haven't found an example of my specific example, so perhaps someone here who has used TinyXML can help out. Below is a slice of the XML, and where I have started to parse it.
XML:
<EGCs xmlns="http://tempuri.org/XMLSchema.xsd">
<card type="EGC1">
<offsets>
[ ... ]
</offsets>
</card>
<card type="EGC2">
<offsets>
[ ... ]
</offsets>
</card>
</EGCs>
Loading/parsing code:
TiXmlDocument doc("EGC_Cards.xml");
if(doc.LoadFile())
{
TiXmlHandle hDoc(&doc);
TiXmlElement* pElem;
TiXmlHandle hRoot(0);
pElem = hDoc.FirstChildElement().Element();
if (!pElem) return false;
hRoot = TiXmlHandle(pElem);
//const char *attribval = hRoot.FirstChild("card").ToElement()->Attribute("card");
pElem = hDoc.FirstChild("EGCs").Child("card", 1).ToElement();
if(pElem)
{
const char* tmp = pElem->GetText();
CComboBox *combo = (CComboBox*)GetDlgItem(IDC_EGC_CARD_TYPE);
combo->AddString(tmp);
}
}
I want to pull out each card "type" and save it to a string to put into a combobox. How do I access this attribute member?
After a lot of playing around with the code, here is the solution! (With help from HERE)
TiXmlDocument doc("EGC_Cards.xml");
combo = (CComboBox*)GetDlgItem(IDC_EGC_CARD_TYPE);
if(doc.LoadFile())
{
TiXmlHandle hDoc(&doc);
TiXmlElement *pRoot, *pParm;
pRoot = doc.FirstChildElement("EGCs");
if(pRoot)
{
pParm = pRoot->FirstChildElement("card");
int i = 0; // for sorting the entries
while(pParm)
{
combo->InsertString(i, pParm->Attribute("type"));
pParm = pParm->NextSiblingElement("card");
i++;
}
}
}
else
{
AfxMessageBox("Could not load XML File.");
return false;
}
there should be a Attribute method that takes and attribut name as parameter see: http://www.grinninglizard.com/tinyxmldocs/classTiXmlElement.html
from the documentation I see the code would look like:
hRoot.FirstChildElement("card").ToElement()->Attibute("type");
However for the type of thing you are doing I would use XPATH if at all possible. I have never used it but the TinyXPath project may be helpful if you choose to go that route the link is: http://tinyxpath.sourceforge.net/
Hope this helps.
The documentation I am using to help you from is found at: http://www.grinninglizard.com/tinyxmldocs/hierarchy.html
What you need is to get the attribute type from the element card. So in your code it should be something like:
const char * attribval = hRoot.FirstChild("card").ToElement()->Attribute("card");

Parsin XML file using pugixml

Hi
I want to use XML file as a config file, from which I will read parameters for my application. I came across on PugiXML library, however I have problem with getting values of attributes.
My XML file looks like that
<?xml version="1.0"?>
<settings>
<deltaDistance> </deltaDistance>
<deltaConvergence>0.25 </deltaConvergence>
<deltaMerging>1.0 </deltaMerging>
<m> 2</m>
<multiplicativeFactor>0.7 </multiplicativeFactor>
<rhoGood> 0.7 </rhoGood>
<rhoMin>0.3 </rhoMin>
<rhoSelect>0.6 </rhoSelect>
<stuckProbability>0.2 </stuckProbability>
<zoneOfInfluenceMin>2.25 </zoneOfInfluenceMin>
</settings>
To pare XML file I use this code
void ReadConfig(char* file)
{
pugi::xml_document doc;
if (!doc.load_file(file)) return false;
pugi::xml_node tools = doc.child("settings");
//[code_traverse_iter
for (pugi::xml_node_iterator it = tools.begin(); it != tools.end(); ++it)
{
cout<<it->name() << " " << it->attribute(it->name()).as_double();
}
}
and I also was trying to use this
void ReadConfig(char* file)
{
pugi::xml_document doc;
if (!doc.load_file(file)) return false;
pugi::xml_node tools = doc.child("settings");
//[code_traverse_iter
for (pugi::xml_node_iterator it = tools.begin(); it != tools.end(); ++it)
{
cout<<it->name() << " " << it->value();
}
}
Attributes are loaded corectly , however all values are equals 0. Could somebody tell me what I do wrong ?
I think your problem is that you're expecting the value to be stored in the node itself, but it's really in a CHILD text node. A quick scan of the documentation showed that you might need
it->child_value()
instead of
it->value()
Are you trying to get all the attributes for a given node or do you want to get the attributes by name?
For the first case, you should be able to use this code:
unsigned int numAttributes = node.attributes();
for (unsigned int nAttribute = 0; nAttribute < numAtributes; ++nAttribute)
{
pug::xml_attribute attrib = node.attribute(nAttribute);
if (!attrib.empty())
{
// process here
}
}
For the second case:
LPCTSTR GetAttribute(pug::xml_node & node, LPCTSTR szAttribName)
{
if (szAttribName == NULL)
return NULL;
pug::xml_attribute attrib = node.attribute(szAttribName);
if (attrib.empty())
return NULL; // or empty string
return attrib.value();
}
If you want stock plain text data into the nodes like
<name> My Name</name>
You need to make it like
rootNode.append_child("name").append_child(node_pcdata).set_value("My name");
If you want to store datatypes, you need to set an attribute. I think what you want is to be able to read the value directly right?
When you are writing the node,
rootNode.append_child("version").append_attribute("value").set_value(0.11)
When you want to read it,
rootNode.child("version").attribute("version").as_double()
At least that's my way of doing it!