Russian Qt Forum

Qt => Уроки и статьи => Тема начата: sergek от Март 28, 2010, 17:43



Название: Статья: О способе работы с XML с использованием SAX-парсера
Отправлено: sergek от Март 28, 2010, 17:43
Коллеги!

В результате работы над проектом наработал небольшой материал, который постарался оформить в виде статьи. Возможно, этот материал заинтересует программистов, работающих с XML-документами в Qt, поскольку я сам в начале проекта столкнулся с трудностями по выбору технического решения. Поэтому считаю нужным поделиться своим опытом.

В следущем сообщении сама статья.

Во вложении - текст статьи и проект примера для Qt SDK 2010.02.1 для Windows.


Название: О способе работы с XML с использованием SAX-парсера
Отправлено: sergek от Март 29, 2010, 09:30
Объектное представление XML-документов
Автор: sergek.
Март, 2010

Введение
В данной статье предлагается простой и достаточно универсальный способ работы с XML-документами в программах C++ с использованием SAX-анализатора, приводятся примеры его использования.

Подход был разработан при реализации библиотеки классов C++ для работы с XML-документами специализированных форматов (http://www.freesoft.ru/?id=680219). Библиотека была предназначена для проектов Qt, поэтому предлагаемый способ также опирается на средства Qt. Соответственно, приводимые здесь примеры взяты из упомянутого проекта. Но, поскольку интерфейс SAX хорошо стандартизован, этот подход можно перенести на другие реализации SAX-анализатора.

Термин «объектное представление» XML-документов, используемый в данной статье, означает то, что содержимое документов описывается в программе C++ в виде классов, и работа с XML-документами, элементами и атрибутами документа в программе сводится к работе с объектами и членами-данными этих объектов. Далее для простоты вместо термина «член-данное» будем использовать «реквизит».

Статья ориентирована на программистов, знакомых с объектно-ориентированным программированием на C++ и принципами работы SAX-анализатора.

Достоинства и ограничения подхода
Главное достоинство предлагаемого способа заключается в чрезвычайной простоте работы с XML-документом. В качестве примера приведем элементарный пример работы со следующим исходным текстом:

Код:
<?xml version="1.0" encoding="windows-1251"?>
<ED EDNo="805253" EDDate="2005-03-01" EDAuthor="4552000000"/>
Фрагмент программного кода, показывающего  работу с атрибутами документа:

Код:
// объект 
CED ed;

// 1. чтение документа (инициализация реквизитов объекта)
ed.readDocument(fileName);

// 2. изменение реквизитов
ed.EDNo = "1";
ed.EDDate = "2010-03-22";
ed.EDAuthor = "4552000001";

// 3. запись измененого XML-документа
ed.writeDocument(fileName);

Выходной документ:
Код:
<?xml version="1.0" encoding="windows-1251"?>
<ED EDNo="1" EDDate="2010-03-22" EDAuthor="4552000001"/>

Этот пример хорошо иллюстрирует цепочку преобразований «XML –> объект –> XML», обеспечивающую последовательное чтение, изменение и запись XML-документа. Объект в середине этой цепочки является представлением документа в удобном для использования в прикладных программах виде.
Естественно, за любое удобство надо платить. В данном случае платой является то, что с помощью таких объектов можно работать только с документами заранее известной структуры. При изменении структуры документов необходимо, кроме участков кода, где используются реквизиты объекта, менять и само объявление класса, описывающего представление документа.

Здесь мы намеренно не касаемся вопросов эффективного использования оперативной памяти – это отдельная задача, которая должна решаться для каждого конкретного случая. Во всяком случае, автору представляется, что предложенное решение в этом отношении ничуть не хуже, чем использование DOM, но обладает большей гибкостью и удобством использования.

Общие принципы объектного представления

Структура класса повторяет структуру XML-документа

Само по себе использование объекта для представления XML-документа никакого выигрыша не дает, все дело в том, как инициализировать реквизиты объекта. Те примеры, которые приведены в [1] или в составе Qt SDK оптимизма не вселяли – организация работы по использованию данных документа в этих примерах возлагалось на обработчики SAX-анализатора: startElement(),endElement() и characters(). Естественно, такое решение для работы с большим набором различных форматов XML-документов не подходило.

Поэтому сразу появилась мысль всю работу по чтению (инициализации) и записи объектов возложить на сами объекты, а обработчики парсера сделать независимыми от формата исходного документа. Сделать это достаточно просто, используя такие замечательные свойства C++, как наследование и полиморфизм. А третий «кит» объектно-ориентированного языка (инкапсуляция) позволит так реализовать классы объектного представления, что будущее (неизбежное!) изменение формата документов уже не будет представляться такой уж сложной задачей.

Итак, вспомним, как SAX-анализатор выполняет разбор XML-документа – он начинает с верхнего (корневого) узла и проходит по дереву, в узлах которого находятся элементы XML-документа. Когда встречается открывающий тег элемента, происходит вызов обработчика startElement(), куда передается список значений атрибутов этого элемента, когда парсер достигает закрывающего тега – вызывается endElement(). Обработка символьных данных выполняется иначе, но, как будет показано ниже, эти отличия не играют существенной роли.

Для выполнения инициализации реквизитов объекта, необходимо, чтобы для каждого структурного элемента XML-документа был поставлен в соответствие структурный элемент класса, описывающего представление. Иными словами, необходимо, чтобы структура класса повторяла структуру XML-документа. Это легко выполнить, если потребовать, чтобы при конструировании классов каждый элемент (узел) исходного документа отображался в свой класс, который назовем узловым классом.

Атрибуты или текстовые элементы исходного документа реализуются в классе в виде членов-данных, вложенные элементы исходного документа – в виде объектов других узловых классов. Как правило, если XML-документ был спроектирован правильно, каждый узловой класс представляет собой некую сущность предметной области, поэтому узловые классы еще называют прикладными.

И, наконец, если у каждого узлового класса будет общий предок, на которого возложим интерфейсные функции, то нетрудно обеспечить, чтобы из обработчиков вызывались соответствующие методы этого интерфейсного класса. Для этого обработчики должны оперировать указателем на интерфейсный класс (да здравствует полиморфизм!).

Узловые классы имеют общего предка

Интерфейс между парсером и объектным представлением XML-документа обеспечивается специальным классом, который, как уже было указано выше, должен быть предком всех узловых (прикладных) классов. Требования к интерфейсному классу (назовем его CNode, префикс «C» от англ. class) диктуются спецификацией SAX-анализатора.

Во-первых, самое очевидное:
·   интерфейсный класс должен предоставить метод инициализации (присвоения) реквизитов объекта.

Атрибуты и текстовые элементы (символьные данные) отображаются в объектном представлении одинаково – в виде реквизитов (членов-данных) класса. Однако обрабатываются они по-разному: атрибуты – в обработчике startElement(), символьные данные – в обработчике endElement(). Дело в том, что парсер передает программе символьные данные посредством обработчика characters(), однако уверенность в том, что данные были переданы полностью, появляется только при достижении парсером конца элемента, содержащего эти данные. Для того, чтобы вызвать интерфейсный метод инициализации для текстового элемента, необходимо знать, что тип этого элемента – текстовый. Таким образом, можно сформулировать второе требование к интерфейсу:

·   в интерфейсе должен быть предусмотрен метод индикации текстовых элементов. Он должен выполнять простую задачу – по имени элемента сообщить, является ли он символьным или нет.

Получив в обработчике  endElement() информацию о том, что текущий элемент был символьным, можно смело вызывать метод инициализации реквизитов.

И, наконец, обработчики должны обращаться к методам конкретного объекта (или его структурной части). Начинается разбор всегда с корневого узла, но по мере продвижения по дереву документа, должен меняться указатель на текущий узел объекта. Таким образом:

·   интерфейсный класс должен иметь метод получения указателя на текущий узел объекта.

Если текущий узел объекта не содержит других объектов, то метод просто возвращает this. В противном случае указатель инициализируется на нужный вложенный узел. Последнее требование должно сопровождаться организацией в обработчиках парсера стека указателей таким образом, чтобы обработчики всегда работали с текущим узлом объекта.

Сформулированные выше требования относятся к взаимодействию объектного представления с SAX-анализатором в процессе чтения (разбора) XML-документа.

Запись документа может выполняться с использованием любых средств, предоставляемых выбранным средством программирования. В Qt такие достаточно удобные средства предоставляет класс QXmlStreamWriter. Реализация записи, учитывая древовидную природу XML, должна быть распределена по иерархии объектного представления, поэтому в интерфейс выделяем еще один метод:
·   метод записи узлового объекта в XML-документ.

Итак, для обеспечения интерфейса с парсером и классом записи документа в интерфейсе CNode должны быть предусмотрены четыре виртуальных метода. Все эти методы должны иметь реализацию по умолчанию, чтобы в порожденных классах выполнять определение только тех методов, какие действительно необходимы. Взаимодействие объектов с парсером осуществляется через средства, представляемые CNode, с обработчиками парсера, которые в данном случае выполнены в виде класса CSaxHandler.

Это, так сказать, обеспечение заявленного универсального подхода. О реализации этих двух классов – в следующей главе.

Реализация подхода

Объем кода, который обеспечивает реализацию предложенного подхода, не очень большой, поэтому он приведен в этом подразделе почти полностью. Из классов удалены лишь некоторые несущественные детали (например, флаги, специфичные для конкретной реализации, обработчик ошибок).
Исходные тексты, приведенные ниже, разбиты на два модуля – cnode.cpp и csaxhandler.cpp.

Интерфейсный класс CNode

Класс CNode является предком всех узловых классов объектного представления, включая корневой узел. Объявление этого класса следующее:

Код:
// cnode.h

#ifndef CNODE_H
#define CNODE_H

#include <QString>

//----------------------------------------------------------------------
// CNode - узел объекта
// Интерфейсный класс, обеспечивающий взаимодействие объекта и XML
//----------------------------------------------------------------------

// Forward Decls
class QXmlAttributes;
class QXmlStreamWriter;
class QIODevice;

class CNode
{
private:
    // вспомогательные методы работы с устройствами записи/чтения
    bool writeToDevice(QIODevice* device);
    bool readFromDevice(QIODevice* device);
protected:
    // пространство имен и префикс элемента
    QString nodeNamespace;
    QString nodePrefix;

    // методы для записи в XML необязательных реквизитов
    void writeAttribute(QXmlStreamWriter& writer,const QString& name, const QString& value);
    void writeTextElement(QXmlStreamWriter& writer,const QString& nsUri,const QString& name,const QString& text);

    // интерфейсные методы - используются для чтения из XML SAX-парсером
    friend class CSaxHandler;
    virtual void setRequisites(const QString &name,const QXmlAttributes &attributes);
    virtual CNode* getNode(const QString &name);
    virtual bool isTextElement(const QString &name);

    // интерфейсный метод - запись объекта в XML
    virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
public:
    CNode();

    // наименование узла
    QString nodeName;

    // чтение объекта из XML - из файла или символьного массива
    bool readDocument(const QString &fileName);
    bool readDocument(QByteArray* array);

    // запись объекта в XML - в файл или символьный массив
    bool writeDocument(const QString &fileName);
    bool writeDocument(QByteArray* array);

    // флаги, используемые при записи
    static QString encoding;   // кодировка, используемая при записи
    static bool autoFormatting;   // флаг форматирования XML при записи
};
//----------------------------------------------------------------------

#endif // CNODE_H

Класс обработчиков парсера CSaxHandler объявлен дружественным, чтобы скрыть интерфейсные методы в защищенной области. Как ранее говорилось, интерфейс должен включать четыре метода:
·   void setRequisites(const QString &name,const QXmlAttributes &attributes) – инициализация реквизитов объекта;
·   CNode* getNode(const QString &name) – получение указателя на объект узлового класса. Метод должен возвращать указатель на объект в случае успеха или 0, если объект с именем name не существует;
·   bool isTextElement(const QString &name) – метод индикации текстовых реквизитов, возвращает true, если реквизит с именем namе является текстовым, и false в противном случае;
·   bool writeNode(QXmlStreamWriter& writer,const QString& nsUri) – запись реквизитов узлового класса. Реализация этого метода в прикладных классах зависит от того, какие средства используются для формирования XML-документа. Ниже приведен пример реализации с использованием класса Qt QXmlStreamWriter.

Интерфейсный класс обеспечивает методами readDocument() и writeDocument() чтение и запись XML-документа в файл или символьный массив QByteArray, которые подключаются в качестве устройств ввода/вывода. Символьный массив играет роль строки, но с более широкими возможностями работы с различными кодировками XML-документов.

Обратите внимание на реквизит nodeName: его необходимо инициализировать в конструкторах прикладных классов именем элементов XML-документов, отображением которых эти классы являются.

Определение класса CNode также не отличается чрезмерной сложностью. Как уговаривались, для базового класса все интерфейсные методы имеют реализации по умолчанию, позволяющие не определять их в наследниках, если в этом нет необходимости:

Код:
// cnode.cpp

#include "cnode.h"

#include "cnode.h"
#include "csaxhandler.h"
#include <QFile>
#include <QBuffer>
#include <QXmlStreamWriter>
//----------------------------------------------------------------------

QString CNode::encoding       = "WINDOWS-1251";
bool    CNode::autoFormatting = true;
//----------------------------------------------------------------------

CNode::CNode(){
}
//----------------------------------------------------------------------
// интерфейсные методы
//----------------------------------------------------------------------

void CNode::setRequisites(const QString &name,const QXmlAttributes &attributes){
     // ничего не делается - для классов, не содержащих реквизиты
}

// указатель на узел элемент
CNode* CNode::getNode(const QString &name){
    if(name==nodeName)
        return this;
    else
        return 0;
}

// проверка, является ли элемент текстовым
bool CNode::isTextElement(const QString &name){
    return false;
}

bool CNode::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
    return true;
}
//----------------------------------------------------------------------
// запись необязательных реквизитов ЭС
//----------------------------------------------------------------------

void CNode::writeAttribute(QXmlStreamWriter& writer,const QString& name, const QString& value){
    if(!value.isEmpty())
        writer.writeAttribute(name, value);
}

void CNode::writeTextElement(QXmlStreamWriter& writer,const QString& nsUri,const QString& name,const QString& text){
    if(!text.isEmpty())
        writer.writeTextElement(nsUri,name,text);
}
//----------------------------------------------------------------------
// чтение из XML (при совпадении типов документа и объекта)
//----------------------------------------------------------------------

bool CNode::readDocument(const QString &fileName){
    QFile device(fileName);
    return readFromDevice(&device);
}

bool CNode::readDocument(QByteArray* array){
    QBuffer device(array);
    return readFromDevice(&device);
}

bool CNode::readFromDevice(QIODevice* device){
    if(!device->open(QIODevice::ReadOnly | QIODevice::Text))
        return false;

    QXmlInputSource xmlInputSource(device);
    CSaxHandler handler(this);

    QXmlSimpleReader reader;
    reader.setContentHandler(&handler);
    bool ok=reader.parse(xmlInputSource);

    device->close();
    return true;
}
//----------------------------------------------------------------------
// запись в XML
//----------------------------------------------------------------------

bool CNode::writeDocument(const QString &fileName){
    QFile device(fileName);
    return writeToDevice(&device);
}

bool CNode::writeDocument(QByteArray* array){
    array->clear();
    QBuffer device(array);
    return writeToDevice(&device);
}

bool CNode::writeToDevice(QIODevice* device){
    QXmlStreamWriter writer(device);

    if(!device->open(QIODevice::WriteOnly))
        return false;

    writer.setAutoFormatting(autoFormatting);

    // формирование xml-документа
    writer.setCodec(encoding.toAscii().data());
    writer.writeStartDocument();
    if(!nodeNamespace.isEmpty())
        writer.writeNamespace(nodeNamespace, nodePrefix);
    // вызов виртуального метода
    writeNode(writer,nodeNamespace);
    writer.writeEndDocument();

    device->close();
    return true;
}

В качестве SAX-анализатора в приведенном коде используется класс Qt QXmlSimpleReader. Для его работы нужны обработчики, которые реализованы в виде класса CSaxHandler и помещены в отдельный модуль. Для записи документа используется, как уже упоминалось, класс Qt QXmlStreamWriter.
Для методов, обеспечивающих чтение и запись XML-документов, необходимо дать некоторые пояснения.

Во-первых, понятно, что метод чтения readDocument() вызывается для уже созданного объекта конкретного типа, и исходный XML-документ должен соответствовать этому типу. Поэтому, в общем случае при чтении не известного заранее документа необходимо сначала определить его тип по имени корневого элемента и создать нужный объект. Это несложно, а то, как это сделать – смотрите в библиотеке QLibUfebs по приведенному выше адресу. Здесь же этот случай не рассматривается.

Что касается записи XML-документа, то в нашем случае для записи атрибутов и текстовых элементов в методах прикладного класса используются, соответственно, методы QXmlStreamWriter::writeAttribute() и QXmlStreamWriter::writeTextElement(). Чтобы облегчить реализацию записи необязательных реквизитов, предусмотрены методы CNode::writeAttribute() и CNode::writeTextElement() с очень похожим синтаксисом, которые формируют атрибут или элемент только для непустых значений.


продолжение - в следующем сообщении.


Название: Re: О способе работы с XML с использованием SAX-парсера
Отправлено: sergek от Март 29, 2010, 09:32
продолжение.

CSaxHandler – класс обработчиков SAX-анализатора

Пока мы будем использовать только три обработчика SAX-анализатора – обработчики начала и конца элементов, а также обработчик символьных данных. Это минимум, который необходим для разбора документа. Дополнительно может потребоваться обработчики ошибок fatalError() и команд обработки processingInstruction(). Последний, в частности, может использоваться для определения кодировки документа, задаваемый в декларации XML атрибутом encoding.

Класс CSaxHandler порожден от класса Qt QxmlDefaultHandler, содержащий весь необходимый набор обработчиков парсера, которые по умолчанию ничего не делают. Для того, чтобы расширить функциональность нашего класса, достаточно добавить в него объявление и реализацию соответствующих методов. Очень удобно.

Код:
// csaxhandler.h

#ifndef CSAXHANDLER_H
#define CSAXHANDLER_H

#include <QXmlDefaultHandler>
#include <QStack>

//----------------------------------------------------------------------
// обработчики для SAX-парсера
//----------------------------------------------------------------------

class CNode;

class CSaxHandler : public QXmlDefaultHandler
{
private:
    CNode* doc;               // указатель на объект
    QStack<CNode*> nodeStack; // стек обрабатываемых элементов
    QString textElement;      // буфер содержимого текстового элемента
    QString encoding;         // кодировка документа
public:
    CSaxHandler();
    CSaxHandler(CNode* node);
    virtual ~CSaxHandler();

    // связывание объекта с обработчиками
    void setDocument(CNode* node);
    void reset();             // очистить стек и буферы

    // обработчики
    bool startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &attributes);
    bool characters(const QString &str);
    bool endElement(const QString &namespaceURI, const QString &localName, const QString &qName);
};
//----------------------------------------------------------------------

#endif // CSAXHANDLER_H

Объект, с которым взаимодействует SAX-анализатор при разборе XML-документа, передается в обработчики в виде указателя doc. Это выполняется либо в конструкторе, либо в явном виде методом setDocument().

В определении класса (ниже) видно, что этот указатель помещается в стек nodeStack. В дальнейшем, по мере продвижения по содержимому документа, в этот стек помещаются и удаляются указатели на узлы объекта. Это обеспечивает работу с вложенными объектами узловых классов синхронно с разбором документа.

Код:
// csaxhandler.cpp

#include "csaxhandler.h"
#include "cnode.h"

//----------------------------------------------------------------------

CSaxHandler::CSaxHandler(){
    reset();
}

CSaxHandler::CSaxHandler(CNode* node){
    setDocument(node);
}

CSaxHandler::~CSaxHandler(){
    // doc не удаляем (владелец - внешняя программа)!
    textElement.clear();
    nodeStack.clear();
}

void CSaxHandler::reset(){
    doc=0;
    textElement.clear();
    nodeStack.clear();
}

void CSaxHandler::setDocument(CNode* node){
    reset();
    doc=node;

    // корневой элемент
    nodeStack.push(doc);
}
//----------------------------------------------------------------------

bool CSaxHandler::startElement(const QString &namespaceURI, const QString &localName, const QString &qName, const QXmlAttributes &attributes){

    if(nodeStack.isEmpty())
        return false;

    // текущий элемент
    CNode* node=nodeStack.top();

    // обрабатываемый элемент
    if(node)
        node=node->getNode(localName);

    // инициализация реквизитов
    if(node)
        node->setRequisites(localName,attributes);

    // сделаем его текущим
    nodeStack.push(node);
    textElement.clear();
    return true;
}
//----------------------------------------------------------------------

bool CSaxHandler::characters(const QString &str){
    textElement+=str;
    return true;
}
//----------------------------------------------------------------------

bool CSaxHandler::endElement(const QString &namespaceURI, const QString &localName, const QString &qName){
    if(nodeStack.isEmpty())
        return false;

    CNode* node=nodeStack.top();

    // инициализация текстовых элементов
    if(node && node->isTextElement(localName)){
        QXmlAttributes textAttr;
        textAttr.append(localName,"","",textElement);
        node->setRequisites(localName,textAttr);
    }

    // элемент обработан
    nodeStack.pop();
    return true;
}

Реквизиты объекта, соответствующие атрибутам исходного документа, инициализируются в обработчике startElement(), реквизиты, соответствующие символьным данным, - в endElement(). Для инициализации используется один и тот же метод интерфейсного класса setRequisites(). Для этого значение текстового элемента записывается в объект класса QXmlAttributes, используемого для передачи атрибутов.

Это искусственный прием, позволяющий сэкономить один метод в интерфейсе CNode. Правда, при этом немного усложняется реализация setRequisites() в узловых классах, поскольку в нем появляется дополнительный условный оператор. Альтернатива – добавление в интерфейс метода инициализации только текстовых реквизитов. Что лучше – судите сами. Автору представляется, что его вариант более экономный.

Собственно, этими двумя классами и ограничивается реализация общего подхода для разбора произвольных XML-документов. Как ими пользоваться – в следующем разделе на примере конкретного документа.

окончание - в следующем сообщении.


Название: Re: О способе работы с XML с использованием SAX-парсера
Отправлено: sergek от Март 29, 2010, 09:32
окончание.

Использование объектного представления

Исходный XML-документ

В качестве исходного документа, для которого будем реализовывать объектное представление, возьмем слегка упрощенный документ специализированного формата ED201 (по сравнению с оригинальным форматом, в нашем документе отсутствуют один атрибут и пара текстовых элементов). Это сделано с целью упрощения иерархии объекта:

Код:
<?xml version="1.0" encoding="WINDOWS-1251"?>
<ED201 xmlns="urn:cbr-ru:ed:v2.0" CtrlCode="0999" CtrlTime="10:13:37" EDNo="805253" EDDate="2010-03-24" EDAuthor="4552000000">
<Annotation>Ошибка при обработке ЭС</Annotation>
<EDRefID EDNo="1000" EDDate="2010-03-24" EDAuthor="4525545000"/>
</ED201>

Пример иерархии прикладных классов

На рисунке представлена статическая UML-диаграмма класса, являющегося объектным представлением нашего XML-документа:
(http://)

Намеренно выбран пример, в котором один из узловых классов (CEDRefID), объект которого включен в качестве члена класса CED201,  используется также и как предок этого класса. Такие структурные решения являются обычным делом в объектном проектировании, и позволяют значительно сэкономить затраты за счет повторного использования кода. И, как можно будет убедиться далее, оказывает влияние на метод записи данных при формировании XML-документа.

Объявления узловых классов

Узловые (прикладные) классы конструируются очень просто:
1)   порождаем их от CNode;
2)   в защищенной части (protected) класса объявляем четыре виртуальных интерфейсных метода. Для классов в конце иерархии наследования их можно объявлять и в закрытой области (private). Есть только особенность, касающаяся метода writeNode() – он вызывается для объектов, являющихся членами других объектов (в документе это – вложенные элементы). В таких случаях есть выбор – либо прятать этот метод и объявлять друзей класса, либо объявлять его в открытой области;
3)   в открытой части объявляем конструктор по умолчанию и реквизиты с именами, совпадающими с именами атрибутов или текстовых элементов. Вложенные элементы объявляются как члены в виде объектов других узловых классов.

Часто бывает, что нет необходимости объявлять некоторые из методов. Например, в CEDRefID нет текстовых элементов, вложенных объектов, поэтому отсутствуют isTextElement() и getNode():

Код:
// cbr_ed201.h

#ifndef   cbr_ed201H
#define   cbr_ed201H

#include "cnode.h"

//----------------------------------------------------------------------

// EDRefID

class CEDRefID : public CNode
{
protected:
    virtual void setRequisites(const QString &name,const QXmlAttributes &attributes);
public:
    virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
    CEDRefID();

    // Methods & Properties
    QString EDNo;
    QString EDDate;
    QString EDAuthor;
};
//----------------------------------------------------------------------

// ED201

class CED201 : public CEDRefID
{
private:
    virtual void setRequisites(const QString &name,const QXmlAttributes &attributes);
    virtual CNode* getNode(const QString &name);
    virtual bool isTextElement(const QString &name);
    virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
public:
    CED201();

    // Methods & Properties
    QString CtrlCode;
    QString CtrlTime;
    QString Annotation;
    CEDRefID EDRefID;
};
//----------------------------------------------------------------------

#endif

Имена реквизитов в классах объявлены с нарушением принятого в C++ стиля именования (с прописной буквы). Это не небрежность автора. Дело в том, что в описании форматов XML-документов, для которых реализованы эти классы, принята именно такая нотация. А в объявление класса они попали методом «copy/paste». И вообще, весь подход объектного представления направлен на то, чтобы процесс конструирования сводился к простым формальным приемам.

Реализация классов

Для наглядности в данном подразделе текст модуля cbr_ed201.cpp разделен на части, с комментариями перед каждой его частью.

В конструкторе узлового класса CEDRefID задаются пространство имен nodeNamespace и его префикс nodePrefix. Это не обязательно. Можно опустить либо оба присвоения (тогда действует ранее объявленное или пространство имен по-умолчанию), либо опустить префикс. Если не задавать префикс, тогда он будет формироваться в соответствии с областью действия пространства имен в форме «n1», «n2» и т.д.:

Код:
// cbr_ed201.cpp

#include "cbr_ed201.h"
#include <QXmlAttributes>
#include <QXmlStreamWriter>
//----------------------------------------------------------------------

// EDRefID
CEDRefID::CEDRefID(){
    // пространство имен
    nodeNamespace = "urn:cbr-ru:ed:v2.0";
    nodePrefix    = "ed";
}

Так выполняется присвоение реквизитов объекта, являющихся аналогом атрибутов XML-документа (для текстовых элементов будет показано ниже):

Код:
// инициализация реквизитов документа при чтении ЭД
void CEDRefID::setRequisites(const QString &,const QXmlAttributes &attributes){
    EDNo=attributes.value("EDNo");
    EDDate=attributes.value("EDDate");
    EDAuthor=attributes.value("EDAuthor");
}

Поскольку EDRefID является элементом исходного документа (узлом), для него определен метод writeNode(), начинающийся с записи открывающего тега writeStartElement() и заканчивающийся записью закрывающего тега writeEndElement():

Код:
bool CEDRefID::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
    writer.writeStartElement(nsUri,nodeName);
    writer.writeAttribute("EDNo", EDNo);
    writer.writeAttribute("EDDate", EDDate);
    writer.writeAttribute("EDAuthor", EDAuthor);
    writer.writeEndElement();
    return true;
}

Для узловых классов задаем имя nodeName, совпадающее с именем открывающего тега элемента исходного документа. Для вложенных элементов надо придерживаться правила – если элементы одинакового типа встречаются в документах с разными именами, то nodeName задается в конструкторе класса-владельца, если везде имена одинаковые – то в своем конструкторе. Однако, чтобы избежать ошибок, предпочтителен первый способ:

Код:
// ED201
CED201::CED201(){
    nodeName="ED201";
    EDRefID.nodeName="EDRefID";
}

В методе setRequisites() приведен пример инициализации текстового реквизита Annotation, об это особенности уже упоминалось выше. Если опустить первое условие, то после инициализации текстового реквизита произойдет очистка остальных реквизитов, т.к. аргумент attributes их не содержит.
Инициализацию реквизитов класса-родителя CEDRefID можно выполнить либо явным образом, как и остальные реквизиты (что может привести к проблемам при изменении формата документа), либо вызовом метода с явным разыменованием (предпочтительно):

Код:
// инициализация реквизитов документа при чтении ЭД
void CED201::setRequisites(const QString &name,const QXmlAttributes &attributes){
    if(name=="Annotation")
        Annotation=attributes.value(name);
    else{
        // инициализация реквизитов базового класса
        CEDRefID::setRequisites(name,attributes);

        CtrlCode=attributes.value("CtrlCode");
        CtrlTime=attributes.value("CtrlTime");
    }
}

Этот метод должен быть определен в двух случаях – если класс содержит вложенные объекты (в нашем случае – EDRefID), либо если в классе есть реквизиты, являющиеся аналогом текстовых элементов (Annotation):

Код:
CNode* CED201::getNode(const QString &name){
    if(name==nodeName || name=="Annotation")
        return this;
    else if(name=="EDRefID")
        return &EDRefID;
    else
        return 0;
}

Для класса, содержащего реквизиты – аналог текстовых элементов, нужно определить этот метод:

Код:
bool CED201::isTextElement(const QString &name){
    return (name=="Annotation");
}

В данном примере есть небольшая особенность. Реквизиты EDNo, EDDate, EDAuthor наследуются от класса СEDRefID, но использовать метод СEDRefID::writeNode() мы не можем, т.к. в этом случае сформируются открывающий и закрывающий теги элемента. Поэтому запись этих реквизитов выполняется так, как если бы они были объявлены в CED201:

Код:
bool CED201::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
    writer.writeStartElement(nsUri,nodeName);
    writer.writeAttribute("EDNo", EDNo);
    writer.writeAttribute("EDDate", EDDate);
    writer.writeAttribute("EDAuthor", EDAuthor);
    writer.writeAttribute("CtrlCode", CtrlCode);
    writer.writeAttribute("CtrlTime", CtrlTime);
    writer.writeTextElement(nsUri,"Annotation", Annotation);
    EDRefID.writeNode(writer,nsUri);
    writer.writeEndElement();
    return true;
}

В заключение о записи необязательных реквизитов. Если какой-либо атрибут может отсутствовать в XML-документе, то его запись нужно выполнять, используя альтернативные методы интерфейсного класса CNode::writeAttribute(),CNode::writeTextElement(). Например, запись
Код:
    writer.writeAttribute("EDNo", EDNo);
надо заменить на следующую:
Код:
    writeAttribute(writer, "EDNo", EDNo);

Использование в прикладной программе

Здесь приведен пример использования сконструированных классов в прикладной программе (в приложении имеется проект для Qt SDK 2010.02.1 for Windows).
Входной документ:

<?xml version="1.0" encoding="WINDOWS-1251"?>
Код:
<ED201 xmlns="urn:cbr-ru:ed:v2.0" CtrlCode="0999" CtrlTime="10:13:37" EDNo="805253" EDDate="2010-03-24" EDAuthor="4552000000">
<Annotation>Ошибка при обработке ЭС</Annotation>
<EDRefID EDNo="1000" EDDate="2010-03-24" EDAuthor="4525545000"/>
</ED201>

Слот xmlSlot() выполняет чтение XML-документа text, содержащегося в текстовом редакторе textEdit, в объект ed. Затем, используя этот объект, выполняется изменение реквизитов и запись объекта в выходной XML-документ out, который добавляется в текстовый редактор для отображения на экране:

Код:
void MainWindow::xmlSlot(){
    QByteArray in;
    QString text=textEdit->toPlainText();
    in.append(text);

    // 1. чтение XML-документа
    CED201 ed;
    ed.readDocument(&in);

    // 2. работа с реквизитами
    ed.EDNo = "1";
    ed.EDDate = "2010-03-01";
    ed.EDAuthor = "4552000001";

    // 3. запись XML-документа
    QByteArray out;
    ed.writeDocument(&out);

    textEdit->append("");
    textEdit->append(out);
}

В результате получаем XML-документ:

Код:
<?xml version="1.0" encoding="windows-1251"?>
<ed:ED201 xmlns:ed="urn:cbr-ru:ed:v2.0" EDNo="1" EDDate="2010-03-01" EDAuthor="4552000001" CtrlCode="0999" CtrlTime="10:13:37">
<Annotation>Ошибка при обработке ЭС</Annotation>
<EDRefID EDNo="1000" EDDate="2010-03-24" EDAuthor="4525545000"/>
</ed:ED201>

Пример для документа с повторяющимися элементами

Случай, когда в XML-документе имеется множественное включение одноименных элементов, достаточно частый, поэтому стоит рассмотреть реализацию объектного представления для таких документов. Например, в качестве примера возьмем документ ED232 (тоже немного упрощенный):

Код:
<?xml version="1.0" encoding="WINDOWS-1251"?>
<ED232 xmlns="urn:cbr-ru:ed:v2.0" EDAuthor="4525000000" EDDate="2008-03-14" EDNo="1005">
<PLAN BS="10101" RKC="2" Type="2"/>
<PLAN BS="10207" RKC="1" Type="2"/>
<PLAN BS="10208" RKC="1" Type="2"/>
</ED232>

Объявление класса для этого документа может выглядеть так (опускаем объявление класса CPLAN):

Код:
class CPLAN;
typedef QVector<CPLAN *> CPLANList;

class CED232 : public CED
{
private:
    virtual CNode* getNode(const QString &name);
    virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
public:
    CED232();
    ~CED232();

    CEDRefID InitialED;
    CPLANList PLAN;
};

Как видно, повторяющаяся часть документа реализована в виде списка с использованием шаблона QVector, аналогичного вектору стандартной библиотеки. В список содержатся указатели на объекты, созданные в памяти. Поэтому для класса CED232 нужен деструктор, освобождающий память, занятую объектами CPLAN:

Код:
CED232::~CED232(){
    for(int i=0; i<PLAN.size(); i++)
        delete PLAN[i];
}

Методы класса можно реализовать так:

Код:
CNode* CED232::getNode(const QString &name){
    if(name==nodeName)
        return this;
    else if(name=="PLAN"){
        CPLAN* info=new CPLAN();
        PLAN.push_back(info);
        return info;
    }else
        return 0;
}
//----------------------------------------------------------------------

bool CED232::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
    writer.writeStartElement(nsUri,nodeName);
    writer.writeAttribute("EDNo", EDNo);
    writer.writeAttribute("EDDate", EDDate);
    writer.writeAttribute("EDAuthor", EDAuthor);

    for(int i=0; i<PLAN.size(); i++)
        PLAN[i]->writeNode(writer,nsUri);

    writer.writeEndElement();
    return true;
}

Заключение

Кому-то может показаться, что объем кода, который нужно определить при использовании предложенного объектного представления XML-документа, больше, чем хотелось бы. Однако, это не так. Например, для того, чтобы использовать так называемые «свойства» классов (property, расширение C++Builder), в реализации аналогичной библиотеки LibUfebs с использованием DOM (http://www.freesoft.ru/?id=673995) приходится определять довольно много кода. К примеру, определение класса CED101 в упомянутой библиотеке занимает около 300 строк, когда как при использовании предлагаемого подхода – всего 120. И это притом, что в DOM не надо заботиться о записи XML-документов в файл.

Правда, справедливости ради надо отметить, что большая  часть кода в C++Builder генерируется автоматически по XSD-схемам специальным инструментом XML Data Binding wizard. Но и ручной работы после этого остается достаточно.

Список использованной литературы
1. Мартин Д., Бирбек М., Кэй М. и др. XML для профессионалов. – М.: Лори, 2001. – 900 с.
2. Qt 4.6.2 Reference Documentation. Copyright © 2010 Nokia Corporation and/or its subsidiary(-ies).

***


Название: Re: О способе работы с XML с использованием SAX-парсера
Отправлено: xintrea от Май 19, 2010, 08:29
SABROG (http://www.prog.org.ru/index.php?action=profile;u=3475) написал:

На 17 страницах, вроде бы с умом подошел человек к делу. Правда иногда кажется, что класс писало несколько человек, стиль программирования не соблюдается во многих местах:

Код
C++ (Qt)
   virtual void setRequisites(const QString &name,const QXmlAttributes &attributes);
   virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);
 
   virtual CNode* getNode(const QString &name);
 

То ссылки с указателями "принадлежат объектам" как в Си, то "типам" как в C++.


Название: Re: Статья: О способе работы с XML с использованием SAX-парсера
Отправлено: sergek от Май 05, 2013, 17:55
За время, прошедшее с первой публикации, в предлагаемом подходе появились небольшие изменения. Во-первых, из интерфейсных методов исключен за ненадобностью CNode::isTextElement(). При этом обработчик конца элемента меняется совсем немного:
Код:
bool CSaxHandler::endElement(const QString &namespaceURI, const QString &localName, const QString &qName){
    if(nodeStack.isEmpty())
        return false;

    CNode* node=nodeStack.top();

    // инициализация текстовых элементов
    if(node && textElement.trimmed().length()){
        QXmlAttributes textAttr;
        textAttr.append(localName,"","",textElement);
        node->setRequisites(localName,textAttr);
        textElement.clear();
    }

    // элемент обработан
    nodeStack.pop();
    return true;
}
Во-вторых, уточнены некоторые особенности работы с текстовыми элементами, содержащими атрибуты (это было упущение), чему посвящена отдельная небольшая глава:

Особенности реализации для текстовых элементов с атрибутами
Текстовые элементы в XML-документах могут содержать атрибуты, например:
Код:
<?xml version="1.0"?>
<CHAPTER cnumber="1">
   <VERS vnumber="1">Содержание текстового элемента</VERS>
</CHAPTER>
В этом случае, элемент оформляется в представлении в виде отдельного узлового класса:
Код:
class CVERS : public CNode
{
protected:
    virtual void setRequisites(const QString &name,const QXmlAttributes &attributes);
    virtual CNode* getNode(const QString &name);
    virtual bool writeNode(QXmlStreamWriter& writer,const QString& nsUri);

public:
    CVERS();

    QString vnumber;
    QString VERS;   // текстовый элемент
};
Для текста элемента в объявлении предусматриваем отдельный реквизит VERS. Имя этого реквизита особого значения не имеет, Например, его можно было бы назвать text, но нет гарантии, что среди атрибутов в будущем не встретится такого имени. Поэтому, предпочтительнее для этого использовать имя элемента (тега).    
Реализация интерфейсных методов может быть такой:
Код:
void CVERS::setRequisites(const QString &name,const QXmlAttributes &attributes){
    // проверка отсутствия текстового реквизита
    if(attributes.value("VERS").isEmpty()){
        vnumber=attributes.value("vnumber");
    } else  // присвоение текстового элемента
        VERS=attributes.value(name);
}
//----------------------------------------------------------------------

bool CVERS::writeNode(QXmlStreamWriter& writer,const QString& nsUri){
    writer.writeStartElement(nsUri,nodeName);

    writer.writeAttribute("vnumber", vnumber);
    writer.writeCharacters(VERS);

    writer.writeEndElement();
    return true;
}
Использовать метод QXmlStreamWriter::writeTextElement() в данном случае нельзя, поскольку этот метод формирует открывающий и закрывающий теги элемента. Поэтому для записи текста используется метод QXmlStreamWriter::writeCharacters().
 
Полный исправленный текст статьи и пример - во вложении.